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.
Related
Hello! I'd appreciate any help in making a connection. Here's what I'm trying to 'Get' info from:
curl https://connect.squareup.com/v2/locations
-H 'Square-Version: 2022-01-20'
-H 'Authorization: Bearer ACCESS_TOKEN'
-H 'Content-Type: application/json'
Setting the Scopes:
First I set the following scopes in the manifest file (here's a picture). I followed a similar notation as google's.
The Apps Script:
function squareLocations() {
var url = "https://connect.squareup.com/v2/locations";
var headers = {
"contentType":"application/json",
"headers": {"Square-Version": "2022-01-20",
"Authorization": "Bearer <TOKEN>"}
};
var data =
{
'locations':
{
'id': locationID,
'name': locationName,
'address': locationAdress,
}
}
var response = UrlFetchApp.fetch(url, headers);
var text = response.getResponseCode();
var json = JSON.parse(response.getContextText());
Logger.log (text);
Logger.log (json);
}
response returns a 400 error: invalid scope (lists all scopes).
In the appsscript.json file remove the OAuth scope from square. On this file only include the OAuth scopes required by the Google Apps Script and Advanced Services (like Advanced Sheets Service).
The scopes of APIs called using by UrlFetchApp.fetch() might be included when generating the token or on the corresponding API console / App settings (I'm sorry , I don't know the details of Square API).
Background: why the manifest scopes are not relevant to the Square API##
The Square API uses the Oauth2 protocol. The idea of this protocol is as follows: you provide the user an opportunity to log in to their Square account, and in the process, you capture an oauth token that represents that user's login. This token allows your script to take action on the user's behalf. The scopes you specify tell the user what types of actions you'll be performing on their behalf, and you are limited to only those actions when calling the API.
The scopes listed in the Apps Script manifest represent the same idea, but for Google's services, not any external services like Square. When you call ScriptApp.getOauthToken(), you get a token for performing actions within the Google account of the user currently running the Apps Script script. (The reason you can do this for Google services without presenting a login screen is that the Google user has already logged in in order to run the script in the first place.)
But for any non-Google API like Square, you need to set up a full OAuth2 process, as detailed below:
The main question: How to access Square's API from Google Apps Script
There is an OAuth2 library for Apps Script that handles most of the mechanics of obtaining and storing the token for any API that uses OAuth2. Start by adding the library to your script (the library's script id is 1B7FSrk5Zi6L1rSxxTDgDEUsPzlukDsi4KGuTMorsTQHhGBzBkMun4iDF).
Then obtain the callback uri for your project. Do this by executing the following function from the script editor:
function logRedirectUri()
{
console.log(
OAuth2.getRedirectUri()
)
}
Now go to the Square developer dashboard, and create a new application. There's an OAuth link on the left side of the app's main screen. Go to that link and you'll get the app's Application ID and Application Secret. You'll need those for your script. In addition, on this screen you should add the redirect uri you obtained above (when the user logs in to Square, the Square API will now know to redirect to this uri, which your script uses to record the oauth token).
Now you're ready to use the OAuth library to provide the Square sign-in process and then call the API. I've created a repo of the code I use for doing this, which you should be able to drop in to your script, but the relevant points are:
In the getSquareService function, set SQUARE_CLIENT_ID and SQUARE_CLIENT_SECRET to the id and secret you got from the Square developer dashboard.
getSquareService is also where you list the scopes you want for the Square API.
function getSquareService()
{
var SQUARE_CLIENT_SECRET = '{your square application secret}'
var SQUARE_CLIENT_ID = '{your square application id}'
return OAuth2.createService('Square')
// Set the endpoint URLs.
.setAuthorizationBaseUrl('https://connect.squareup.com/oauth2/authorize')
.setTokenUrl('https://connect.squareup.com/oauth2/token')
// Set the client ID and secret.
.setClientId(SQUARE_CLIENT_ID)
.setClientSecret(SQUARE_CLIENT_SECRET)
// Set the name of the callback function that should be invoked to
// complete the OAuth flow.
.setCallbackFunction('authCallbackSquare')
// Set the property store where authorized tokens should be persisted.
// Change this to .getUserProperties() if you are having multiple google users authorize the service:
// this will prevent one user's token from being visible to others.
.setPropertyStore(PropertiesService.getScriptProperties())
// Set the scopes needed. For a full list see https://developer.squareup.com/docs/oauth-api/square-permissions
.setScope(
[
'ORDERS_WRITE',
'PAYMENTS_WRITE',
'PAYMENTS_READ',
'ORDERS_READ',
'MERCHANT_PROFILE_READ'
]
.join(" ")
)
// Set grant type
.setGrantType('authorization_code')
}
And include the callback function that will store the token:
function authCallbackSquare(request)
{
var service = getSquareService();
// Now process request
var authorized = service.handleCallback(request);
if (authorized)
{
return HtmlService.createHtmlOutput('Success! You can close this tab.');
} else
{
return HtmlService.createHtmlOutput('Denied. You can close this tab.');
}
}
If your script is bound to a spreadsheet, you can run authorizeSquareUser() in the repo to get a sidebar that initiates the authorization flow. Otherwise, run this modified version:
function authorizeSquareUser()
{
var service = getSquareService();
var authorizationUrl = service.getAuthorizationUrl();
console.log("Auth URL is %s", authorizationUrl);
}
and then visit the url that is logged. At this url you will log in to your square account, and then you should be redirected to a page that says "Success! You can close this tab."
At this point, the Square user's OAuth token has been stored in the script properties of your project. You are now ready to make a call to the Square API. When doing this, you access the stored token using getSquareService().getAccessToken(), so your headers will look like
var headers = {
'Authorization': 'Bearer ' + getSquareService().getAccessToken(),
'Square-Version': '2022-01-20',
'Content-Type': 'application/json'
}
Then you can call
UrlFetchApp.fetch(url, {'method': 'GET', 'headers': headers}) // Change GET method depending on the action you are performing
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.
I'm currently trying to make a google chat bot on Apps Script, using webhooks that supposedly don't require any authentification.
However, when I run my Apps Script function from Apps Script, it seems like I have to allow my account to "use external web apps" or something along these lines.
I developped the Apps Script as an API, so that I could call the functions from an external point, but it says that I need OAuth credentials to do so. And I know how to implement OAuth credentials, I just have no idea what scope I'm supposed to use since webhooks are supposed to work without authentification.
Here's the function:
function sendText(text) {
var url = "https://chat.googleapis.com/v1/spaces/[space]/messages?key=[webhook-key]&token=[token]";
message = { 'text': text };
var message_headers = {};
const response = UrlFetchApp.fetch(url, {
method: 'post',
contentType: 'application/json',
payload: JSON.stringify(message),
});
Logger.log(response);
}
okay I found the answer myself;
scope is : https://googleapis.com/auth/script.external_request
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.
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>