How to assign UrlFetchApp with basic authorization to button? - google-apps-script

I created function in Google Apps Script, that works well when I run it in Google Apps Script. Output data returns to Google Sheets.
function testFunction11() {
var rng = SpreadsheetApp.getActiveRange();
var encodedAuthInformation = Utilities.base64Encode("username:key");
var headers = {"Authorization" : "Basic " + encodedAuthInformation};
var params = {
'method': 'GET',
'muteHttpExceptions': true,
'headers': headers
};
var res = UrlFetchApp.fetch("https://api.apiservice.com/api/v1/xxx?fields=somefields", params);
Logger.log(res.getContentText());
rng.setValue(res);
}
Output in cell:
[
{
"id": xxx,
"createdDate": "2019-02-01T04:54:00Z",
"reference": "XXX"
},
etc
Then I assign script to button, 'testFunction11'.
And when I click button, it returns
{
"message": "An error has occurred."
}
It looks like response from API server.
My only hypothesis is that google sheet's button adds some headers, User-Agent or content-type to request, which not allowed in API server. And after some search, I guess I can't reassign User-Agent in request. Is that something right or I do it wrong?
EDIT 1:
Headers for each case console.log(UrlFetchApp.getRequest(url, params)):
When clicking button in spreadsheet:
{headers={Authorization=Basic XXXXXXXXQVU6MWVhODlmZmFkN2U3NGNjOGJkOTc1YTE1ZjVhNTE3MzE=, X-Forwarded-For=178.xx.my.ip}, method=get, payload=, followRedirects=true, validateHttpsCertificates=true, useIntranet=false, contentType=null, url=https://api.apisite.com/api/v1/SalesOrders?fields=Id,Createddate,Reference&where=Createddate%3E2019-02-01T00:00:00Z}
And for script:
{headers={Authorization=Basic XXXXXXXXQVU6MWVhODlmZmFkN2U3NGNjOGJkOTc1YTE1ZjVhNTE3MzE=}, method=get, payload=, followRedirects=true, validateHttpsCertificates=true, useIntranet=false, contentType=null, url=https://api.apisite.com/api/v1/SalesOrders?fields=Id,Createddate,Reference&where=Createddate%3E2019-02-01T00:00:00Z}
So the button only adds X-Forwarded-For.
When I tried manually add X-Forwarded-For: 'unknown' there are error like this
There are attribute with impossible value: Header:X-Forwarded-For
Text of error in russian, so sorry for maybe not accurate translating. It's fun, because when I added Test: unknown in same way, there are no error, but obviously not working. Looks like google don't allow to change this value.
Will try different headers in postman and maybe confirm that this header is the cause of the error. Thank you #TheMaster
EDIT 2:
I tried different headers via Postman. So, result is when I add to headers X-Forwarded-For key with any value, it return "message": "An error has occurred."
When I don't add this key, it works well.
So, the question is any way to disable adding this header via Google Apps Script. It seems like not.

In your situation, when UrlFetchApp.fetch() is run from a button on Spreadsheet, X-Forwarded-For is automatically added to the header.
By added X-Forwarded-For to the header, the error of An error has occurred. occurs.
On the other hand, X-Forwarded-For is not used in the header, no error occurs.
If my understanding is correct, how about this workaround? I think that there might be several workarounds. So please think of this as just one of them. In this workaround, Web Apps is used as a wrapper function.
Sample script:
At first, please copy and paste the following script. And please set testFunction11() to the button. When testFunction11() is run, testFunction11() requests to Web Apps (doGet()), and Web Apps requests to https://api.apiservice.com/api/v1/xxx?fields=somefields. By this, X-Forwarded-For is not used to the header of request. Then, the result of Web Apps is returned and put the value to the spreadsheet. Please deploy Web Apps, before you run the script.
function testFunction11() {
var rng = SpreadsheetApp.getActiveRange();
var url = ScriptApp.getService().getUrl();
var params = {method: "get", headers: {Authorization: "Bearer " + ScriptApp.getOAuthToken()}};
var res = UrlFetchApp.fetch(url, params);
Logger.log(res.getContentText());
rng.setValue(res);
}
function doGet() {
var url = "https://api.apiservice.com/api/v1/xxx?fields=somefields";
var encodedAuthInformation = Utilities.base64Encode("username:key");
var headers = {"Authorization" : "Basic " + encodedAuthInformation};
var params = {
'method': 'GET',
'muteHttpExceptions': true,
'headers': headers
};
var res = UrlFetchApp.fetch(url, params);
return ContentService.createTextOutput(res.getContentText());
}
Deploy Web Apps:
Before you run this script, please deploy Web Apps.
On the Script Editor
Publish -> Deploy as Web App
Create new Project version
At "Execute the app a"s, select "Me"
At "Who has access to the app",
if the function is used by only you, select "Only myself".
if the function is used by several users, select "Anyone".
Click "Deploy"
Copy "Current web app URL"
Click "OK"
Note:
When you modified the script, please redeploy Web Apps as new version. By this, the latest script is reflected to Web Apps. This is an important point.
This is a simple sample script. So please modify to your situation.
References:
Web Apps
Taking advantage of Web Apps with Google Apps Script

For future searches on this topic: I had an issue with the exact same error message, where I was using UrlFetchApp#getRequest as a utility to generate a request object, then sending it with UrlFetchApp#fetchAll.
The solution was to avoid using getRequest and instead build the request by myself without using any X-Forwarded-For header, which allowed the request to complete successfully.
I can't find any documentation to support this, but I would speculate that the following issue occurred: when Google injects the X-Forwarded-For header before making a request, the IP it injects depends on the context the script gets executed in. I think Google validates that if that header is set manually, then the IP address needs to match either their GAS proxy's IP or the user's browser IP; which one to use seems to change depending on the context the script is executed in.

Related

Error "302 Moved Temporarily" in "Test as add-on" mode (Telegram)

The spreadsheet contains project 1, deployed as a webapp with permissions: Execute as: Me, Who has access: Anyone.
Webapp
function doPost(e) {
myLog('Received from Addon: ' + JSON.stringify(e));
// console.log('parameters from caller ' + JSON.stringify(e));
return ContentService.createTextOutput(JSON.stringify(e));
}
A webhook aTelegram-bot and this webapp is set.
I am using this spreadsheet for testing (as add-on) of another project 2.
Add-on
function sendPost() {
var sheetURL = SpreadsheetApp.getActiveSpreadsheet().getUrl();
// var webAppUrl = "https://script.google.com/macros/s/#####/exec"; // 7: Part_1 - WebApp: My
var webAppUrl = "https://script.google.com/macros/s/###/exec"; // 7: Part_1 - WebApp: Tester
// var auth = ScriptApp.getOAuthToken();
// var header = { 'Authorization': 'Bearer ' + auth };
var payload = { scriptName: 'updateData', sheetURL: 'sheetURL' };
var options = {
method: 'post',
// headers: header,
muteHttpExceptions: true,
payload: payload
};
var resp = UrlFetchApp.fetch(webAppUrl, options);
var respCode = resp.getResponseCode();
console.log('resp: ' + respCode);
myLog(respCode);
var respTxt = resp.getContentText();
myLog('Response from webApp: ' + respTxt);
console.log('resp: ' + respTxt);
}
Here is a short video of the process (EN-subtitles).
I run sendPost() and everything works fine. Project 2 sends data to the webapp, which returns it. Since this is a Container-bound script and not a standalone one, I cannot watch the logs in the GCC logger. Therefore, I look at them in the custom logger and the entries are added normally.
Also https://api.telegram.org/bot{API_token}/getWebhookInfo shows that there are no errors:
{"ok":true,"result": {"url":"https://script.google.com/macros/s/###/exec", "has_custom_certificate":false, "pending_update_count":0, "max_connections":40,"ip_address":"142.250.***.***"}}
Now I am sending a message from the chat with the bot. The doPost(e) function in the webapp accepts it and writes it to the spreadsheet.
However, everything is not limited to one message. Requests from the bot come and come, and the logger creates more and more new rows in the spreadsheet. This happens until I redeploy the webapp with the doPost () function commented out. I tried to figure out if this is a limited loop or not. My patience was only enough for 20 such iterations, because as a result, the messages start repeating at intervals of about 1 minute. Then I have to reinstall the webhook.
In any case, it interferes with testing the addon.
GetWebhookInfo is now showing that there is a "Wrong response from the webhook: 302 Moved Temporarily" error:
{"ok":true,"result": {"url":"https://script.google.com/macros/s/###/exec", "has_custom_certificate":false, "pending_update_count":1, "last_error_date":1635501472, "last_error_message":"Wrong response from the webhook: 302 Moved Temporarily", "max_connections":40,"ip_address":"142.250.1***.***"}}
Googling revealed several possible reasons. From url to the script has changed to MITM in your network.
I do not really believe in MITM and I suppose that this is due to the fact that the spreadsheet is open in testing mode as add-on and the URL of the webapp has changed in this mode. If so, then I'm not sure if this is the correct behavior of the testing system. In theory, such a situation should have been provided for and the webap url should remain unchanged. But maybe I'm wrong and the reason is different, so
QUESTION:
Has anyone come across such a situation and will suggest a workaround on how to test a script as an addon in such conditions?
http-status-code-302 refers to redirection. If ContentService is used, Google temporarily redirects the resource to a another domain to serve the content. This redirection is not performed when using HtmlService. So, if the issue is related to redirection, use HtmlService instead.

Google app script runs in debug mode, not in run mode

Hello Google Script experts, I'm new to google script and trying something related to HTTP POST from google docs.
I've a google app script that sends a post request (API) on opening a google doc.
function onOpen() {
var ui = DocumentApp.getUi(); // Same variations.
var actDoc = DocumentApp.getActiveDocument();
var docID = actDoc.getId();
var repeater = 0;
var data = {
'bstask': text,
'docid': docID
};
console.log(data);
var options = {
'method' : 'post',
'contentType': 'application/json',
// Convert the JavaScript object to a JSON string.
'payload' : JSON.stringify(data)
};
ui.alert("Sending request with the payload" + data.bstask + " and " + data.docid);
var response = UrlFetchApp.fetch('https://test.com/path', options);
ui.alert("Response is: " + response.getResponseCode());
if (response.getResponseCode() == 200) {
Logger.log(response.getContentText() + response.getAllHeaders());
ui.alert('Yey!! Document refreshed.');
}
else{
ui.alert('Opps!! Document refresh failed.'+ response.getContent());
}
}
This script is used to provide an option to the user to update the document when it is opened or refreshed. This script runs fine and invoking the API when I test it in debug mode. But, when I want this script to be executed on opening the document, it is not invoking the API and the rest of the UI prompts are working fine. Am I missing something here or something wrong with the script? I really appreciate any help!!
Simple Triggers – Restrictions
Because simple triggers fire automatically, without asking the user for authorization, they are subject to several restrictions:
They cannot access services that require authorization. For example, a simple trigger cannot send an email because the Gmail service requires authorization, but a simple trigger can translate a phrase with the Language service, which is anonymous.
In other words, the UrlFetchApp.fetch() call will not execute when opening the document because it requires authorization. You should be able to see this failure in the execution logs as well.

Programmatically connect Sheets / request access

Is there a way for Google Script to request access to a Google Sheet using the Document Key? I've tried openById() and was hoping for it to trigger a request, but nope...
I have found a way which seems to work for me, but I will caveat it with the fact that it is not documented, so neither likely to be supported or recommended, and I imagine it could probably change at any time.
It requires making OAuth2 authenticated requests with the server, so there are a number of steps involved:
Ensure Drive is an enabled scope on your script
Under File > Project Properties > Scopes, check that the following scope is listed:
https://www.googleapis.com/auth/drive
If it isn't listed then running a simple call like:
DriveApp.createFile('', '');
Should prompt you to allow access to Drive. Once done, you can remove this line from your script.
Verify that the required Drive scope is now listed.
Make a request for the file you wish access for
This sample function worked for me, but YMMV:
function requestShare(spreadsheetId) {
var request = {
requestType: 'requestAccess',
itemIds: spreadsheetId,
foreignService: 'explorer',
shareService: 'explorer',
authuser: 0
};
var url = 'https://docs.google.com/sharing/commonshare';
var params = {
method: 'POST',
payload: request,
headers: {
Authorization: 'Bearer ' + ScriptApp.getOAuthToken()
},
contentType: 'application/x-www-form-urlencoded'
};
var response = UrlFetchApp.fetch(url, params);
Logger.log(response.getContentText());
}
Call the requestShare function with the ID of the spreadsheet you are wishing to request access to.
I tested this between two accounts, and received sharing request emails, which when clicked, showed the dialog with the correct requestor details in.
Hope this helps.

Calling a Google Apps Script web app with access token

I need to execute a GAS service on behalf of a user that is logged to my system. So I have her/his access token. I would like somehow to transfer the token to the web app and without having to authorize again the user to use it for some activities. Can this be accomplished? Thank you.
EDIT: I think I didn't explain right what I try to accomplish. Here is the work flow I try to achieve:
We authorize a user visiting our website using OAuth2 and Google;
We get hold of her/his access token that Google returns;
There is a Google Apps Script web app that is executed as the user running the web app;
We want to call this app (3) by providing the access token (2) so Google not to ask again for authorization;
Actually, we want to call this app (3) not by redirecting the user to it but by calling it as a web service.
Thanks
Martin's answer worked for me in the end, but when I was making a prototype there was a major hurdle.
I needed to add the following scope manually, as the "automatic scope detection system" of google apps script did not ask for it: "https://www.googleapis.com/auth/drive.readonly". This resulted in UrlFetchApp.fetch always giving 401 with additional information I did not understand. Logging this additional information would show html, including the following string
Sorry, unable to open the file at this time.</p><p> Please check the address and try again.
I still don't really understand why "https://www.googleapis.com/auth/drive.readonly" would be necessary. It may have to do with the fact that we can use the /dev url, but who may use the /dev url is managed is checked using the drive permissions of the script file.
That said, the following setup then works for me (it also works with doGet etc, but I chose doPost). I chose to list the minimally needed scopes explicitly in the manifest file, but you can also make sure the calling script will ask for permissions to access drive in different ways. We have two google apps script projects, Caller and WebApp.
In the manifest file of Caller, i.e. appsscript.json
{
...
"oauthScopes":
[
"https://www.googleapis.com/auth/drive.readonly",
"https://www.googleapis.com/auth/script.external_request"]
}
In Code.gs of Caller
function controlCallSimpleService(){
var webAppUrl ='https://script.google.com/a/DOMAIN/macros/s/id123123123/exec';
// var webAppUrl =
// 'https://script.google.com/a/DOMAIN/macros/s/id1212121212/dev'
var token = ScriptApp.getOAuthToken();
var options = {
'method' : 'post'
, 'headers': {'Authorization': 'Bearer '+ token}
, muteHttpExceptions: true
};
var response = UrlFetchApp.fetch(webAppUrl, options);
Logger.log(response.getContentText());
}
In Code.gs of WebApp (the web app being called)
function doPost(event){
return ContentService.createTextOutput("Hello World");
}
The hard answer is NO you can't use the built-in services of Apps Script with a service token. But if you already have the token for a user generated by a service account, access to the users data is pretty similar to any other language. All calls would be to the REST interface of the service your token is scoped for.
Take this small script for example. It will build a list of all the user's folders and return them as JSON:
function doGet(e){
var token = e.parameter.token;
var folderArray = [];
var pageToken = "";
var query = encodeURIComponent("mimeType = 'application/vnd.google-apps.folder'");
var params = {method:"GET",
contentType:'application/json',
headers:{Authorization:"Bearer "+token},
muteHttpExceptions:true
};
var url = "https://www.googleapis.com/drive/v2/files?q="+query;
do{
var results = UrlFetchApp.fetch(url,params);
if(results.getResponseCode() != 200){
Logger.log(results);
break;
}
var folders = JSON.parse(results.getContentText());
url = "https://www.googleapis.com/drive/v2/files?q="+query;
for(var i in folders.items){
folderArray.push({"name":folders.items[i].title, "id":folders.items[i].id})
}
pageToken = folders.nextPageToken;
url += "&pageToken="+encodeURIComponent(pageToken);
}while(pageToken != undefined)
var folderObj = {};
folderObj["folders"] = folderArray;
return ContentService.createTextOutput(JSON.stringify(folderObj)).setMimeType(ContentService.MimeType.JSON);
}
You do miss out on a lot of the convenience that makes Apps Script so powerful, mainly the built in services, but all functionality is available through the Google REST APIs.
I found a way! Just include the following header in the request:
Authorization: Bearer <user's_access_token>

How to authorize and post/update Trello card from a Google docs script

I have a Google Docs Spreadsheet that I'd like to use to update referenced cards in Trello. I've had some success with oauth and pulling data via their HTTP API, but am stuck with the following:
1) it seems Trello's code.js requires a window object, which the Google Doc script doesn't provide. So, I am stuck using their HTTP API.
2) authenticating via OAuth works, but only gives me read access. I cannot update cards with the token I am able to get.
function test() {
var oauthConfig = UrlFetchApp.addOAuthService("trello");
oauthConfig.setAccessTokenUrl("https://trello.com/1/OAuthGetAccessToken");
oauthConfig.setRequestTokenUrl("https://trello.com/1/OAuthGetRequestToken");
oauthConfig.setAuthorizationUrl("https://trello.com/1/authorize?key=" + consumerKey + "&name=trello&expiration=never&response_type=token&scope=read,write");
//oauthConfig.setAuthorizationUrl("https://trello.com/1/OAuthAuthorizeToken"); <-- this only gives read access. Cannot POST
oauthConfig.setConsumerKey(consumerKey);
oauthConfig.setConsumerSecret(consumerSecret);
var url = 'https://trello.com/1/cards/yOqEgvzb/actions/comments&text=Testing...';
var postOptions = {"method" : "post",
"oAuthServiceName": "trello",
"oAuthUseToken": "always"};
var response = UrlFetchApp.fetch(url, postOptions); // "Request failed for returned code 404. Truncated server response: Cannot POST"
Logger.log(response.getContentText());
}
I've found a number of related questions but no direct answers:
How to get a permanent user token for writes using the Trello API?
Trello API: vote on a card
Trello API: How to POST a Card from Google Apps Script (GAS)
Google apps script oauth connect doesn't work with trello
Many thanks ahead of time for any advice.
In order to get write access, you need to change the authorization url.
This example works for me
var oauthConfig = UrlFetchApp.addOAuthService("trello");
oauthConfig.setAccessTokenUrl("https://trello.com/1/OAuthGetAccessToken");
oauthConfig.setRequestTokenUrl("https://trello.com/1/OAuthGetRequestToken");
oauthConfig.setAuthorizationUrl("https://trello.com/1/OAuthAuthorizeToken?scope=read,write");
on 1) yes you cant use the library from server gas, its meant to be run from a browser.
on 2), Ive done it from GAS with write access without problems. You need to use the format:
https://api.trello.com/1/.../xxxx?key=yyyyyy&token=zzzzzzz&...
and when you get the token, you need to request permanent access (no expiration) and write access, as in:
https://trello.com/1/authorize?key="+key+"&name=xxxxxxx&expiration=never&response_type=token&scope=read,write"
As in:
function postNewCardCommentWorker(cardId, comment, key, token) {
var commentEncoded=encodeURIComponent(comment);
var url = "https://api.trello.com/1/cards/"+cardId+"/actions/comments?text="+commentEncoded+"&key="+key+"&token="+token;
var options =
{
"method" : "POST"
};
UrlFetchApp.fetch(url, options);
}