Calling a Google Apps Script web app with access token - google-apps-script

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>

Related

Create apps script service accounts to access Drive, Sheet, and Docs

I am in a similar situation to the OP of this post:
User access request when GAS run as the user
I need to run a web app as an 'active user', allow this user to access Drive, Docs, and Sheets resources, but not having the user direct access to them.
However my knowledge is much less on the subject.
As I understand it, I need to create a service account so that the script running as the 'active user' can access Drive, Sheet, and Docs resources that the active user does not have access to.
I am also looking at other resources as well as Google's documentation, but it's a bit overwhelming.
Can anyone explain the basics for this? Maybe a tutorial (or a link to such) that really inexperienced users can understand? I just need to get started on the right direction.
Thank you in advance!
Impersonation of users using App Script
It should be possible to generate a key and start the process of impersonation and call off the scopes and API.
function getJWT(sub) {
var header = { "alg": "RS256", "typ": "JWT" }
var encodedheader = Utilities.base64EncodeWebSafe(JSON.stringify(header))
var key = "-----BEGIN PRIVATE KEY----- fjsklfjl;sdjfasd -----END PRIVATE KEY-----\n"
var time = Math.floor(new Date().getTime() / 1000)
var claim = {
"iss": "yourserviceaccount#mail-p-any.iam.gserviceaccount.com",
"scope": "https://mail.google.com/",
"aud": "https://oauth2.googleapis.com/token",
"iat": time,
"exp": time + 3600,
"sub": sub[0]
}
var encodedclaim = Utilities.base64EncodeWebSafe(JSON.stringify(claim))
var input = encodedheader + "." + encodedclaim
var signed = Utilities.computeRsaSha256Signature(input, key)
var base64signed = Utilities.base64Encode(signed)
var jwt = encodedheader + "." + encodedclaim + "." + base64signed
return jwt
}
function getAccessToken(user) {
var payload = {
"grant_type": "urn:ietf:params:oauth:grant-type:jwt-bearer",
"assertion": getJWT(user)
}
var params = {
"method": "POST",
"contentType": "application/x-www-form-urlencoded",
"payload": payload,
"muteHttpExceptions": true
}
var response = UrlFetchApp.fetch("https://oauth2.googleapis.com/token", params)
var output = JSON.parse(response.getContentText())
console.log(output.access_token)
return output.access_token
}
You can also review the library and step by step process on how you can implement it in another way from here:
https://github.com/googleworkspace/apps-script-oauth2
My code sample was based on the sample script from:
https://developers.google.com/identity/protocols/oauth2/service-account#delegatingauthority
You can also review the other sample code from the references below.
This way you are able to impersonate the user and run or make calls on behalf of the user from your organization without having access to it. This might be where you can start your idea on how to start.
References
https://cloud.google.com/iam/docs/impersonating-service-accounts
https://github.com/googleworkspace/apps-script-oauth2/blob/main/samples/GoogleServiceAccount.gs
I got this to work, for the benefit of those who are the same level in this subject as I am, and in the similar situation. Anyone please expound or correct me if I'm wrong, thanks.
You cannot use the methods to access Drive, Docs, and Sheets in the
same code that runs as the 'active user'.
You have to access these Google services using the equivalent HTTP
API calls of the methods.
The HTTP API calls need a user that would interact with the resources
(because it's being called from publicly from the internet and not
from the script).
You create a service account for this. This acts as the user for the
calls.
I started with Ricardo Jose Velasquez Cruz's response, and found other resources, as I was calling the API from Apps Script.
https://medium.com/geekculture/how-to-use-service-accounts-and-oauth2-in-google-apps-script-99c4bc91dc31
Note that Apps Script requires an OAUTH2 library to connect, not sure why this was not built-in to GAS itself:
https://github.com/googleworkspace/apps-script-oauth2
How to create a service account and use it to access Google Drive (you use the same code to access Docs and Sheet as well, you just need to use the corresponding URL and parameters for the services):
https://www.labnol.org/code/20375-service-accounts-google-apps-script
it's basically the same code as another post I found here:
Google Service Accounts / API - I keep getting the Error: Access not granted or expired. (line 454, file "Service")
Hope this helps :)

How do you call Square's APIs to Google Sheets using Google Apps Script?

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

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.

Use Google Script's Web App as Webhook to receive Push Notification directly

My Goal: Changes in Google Drive => Push Notification to https://script.google.com/a/macros/my-domain/... => App is pushed to take action.
I don't want to setup an middle Webhook agent for receiving notification. Instead, let the Web App (by Google Script) to receive it and be pushed directly.
Since the relevant function is quite undocumented (just here: https://developers.google.com/drive/web/push) , below is the code I tried but failure.
1. Is above idea feasible??
2. My code doPost(R) seems cannot receive notification (R parameter) properly. Anyway, no response after I change the Google Drive. Any problem? (I have tried to log the input parameter R so as to see its real structure and decide if the parameter Obj for OAuth is the same as normal Drive App, but error occur before log)
function SetWatchByOnce(){
var Channel = {
'address': 'https://script.google.com/a/macros/my-domain/.../exec',
'type': 'web_hook',
'id': 'my-UUID'
};
var Result = Drive.Changes.watch(Channel);
...
}
function doPost(R) {
var SysEmail = "My Email";
MailApp.sendEmail(SysEmail, 'Testing ', 'Successfully to received Push Notification');
var Response = JSON.parse(R.parameters);
if (Response.kind == "drive#add") {
var FileId = Response.fileId;
MyFile = DriveApp.getFolderById(FileId);
...
}
}
function doGet(e) {
var HTMLToOutput;
var SysEmail = "My Email";
if (e.parameters.kind) {
//I think this part is not needed, since Push Notification by Drive is via Post, not Get. I should use onPost() to receive it. Right?
} else if (e.parameters.code) {
getAndStoreAccessToken(e.parameters.code);
HTMLToOutput = '<html><h1>App is successfully installed.</h1></html>';
} else { //we are starting from scratch or resetting
HTMLToOutput = "<html><h1>Install this App now...!</h1><a href='" + getURLForAuthorization() + "'>click here to start</a></html>";
}
return HtmlService.createHtmlOutput(HTMLToOutput);
}
....
Cloud Functions HTTP trigger(s) might also be an option ...
(which not yet existed at time of this question). this just requires setting the trigger URL as the notification URL, in the Google Drive settings - and adding some NodeJS code for the trigger; whatever it shall do. one can eg. send emails and/or FCM push notifications alike that. that trigger could also be triggered from App Script, with UrlFetchApp and there is the App Script API. one can have several triggers, which are performing different tasks (App Script is only one possibilty).
Cicada,
We have done similar functions to receive webhooks/API calls many times. Notes:
to get R, you need: var Response = R.parameters and then you can do Response.kind, Response.id, etc.
Logger will not work with doGet() and doPost(). I set it up a write to spreadsheet -- before any serious code. That way I know if it is getting triggered.

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);
}