Configure Google Apps Script callback for Slack slash command - google-apps-script

TL;DR - I would like to setup a Slack slash command that works on Google Apps Script. How do you recommend I do so?
Context:
I currently use google.com/script with Sheets to trigger a webhook call to Slack when someone fills out a Google Form. Here it is for reference. My question is an additional feature I'd like to add that won't overlap with this code.
I want to listen in Google Apps for a Slack slash command, so I've been looking through tons of examples of callbacks. I don't understand the dance necessary to get this going. Here are the variables:
I've setup the Slack slash command, which asks for a URL and method type (POST or GET), then gives me a token for verification of the outgoing payload.
I've setup OAuth 2.0 client IDs on Google APIs, which provides a client ID and secret token.
The Google Apps tutorial here offers https://script.google.com/macros/d/{SCRIPT ID}/usercallback, but if I'm reading it correctly in this guide it would require user authorization
I've reviewed other questions that mention callback (this and this) but haven't found anything relevant.
Guidance toward the next steps of just being able to configure a callback URL and understand how it will authentication and ultimately consume the API call from Slack in Google would be awesome.

You are on the right track, i'm fiddling around with this since couple of weeks so maybe this helps:
Google App Script
Publish > Deploy as Web App > Version: New / Execute the App as : Me / Who has access: Anyone Even Anonymous
You'll get a url for that web app something like https://script.google.com/macros/s/xxx-ver-long-number-xxx/exec
Copy that
Slack Integrations
Create new Slash Command
Method POST / URL: Paste The url from your web app
Copy the Secure Token, you need it in the Google App Script
Set all the other settings as you like.
Don't forget to save
Back to Google App Script
Copy paste this basic stuff
function doPost(request) {
//// SET SECURITY TOKEN (FROM SLACK COMMAND)
var your_token = "YOUR_SLASH_COMMAND_TOKEN";
var output;
//// GET PARAMETES FROM SLACK POST REQUEST
var params = request.parameters;
//// ... and store them into variables
var cmd_token = params.token; // or params.token[0] idk
//// CHECK RECEIVED TOKEN AGAINST YOUR SAVED TOKEN
if (your_token == cmd_token) {
output = {"text":"SUCCESS"};
} else {
output = {"text":"INVALID TOKEN"};
}
//// SEND RESPONSE BACK TO SLACK
return ContentService.createTextOutput(JSON.stringify(output)).setMimeType(ContentService.MimeType.JSON);
}
Save script and again publish the script as web app (you have to do this everytime you change something, always choose "new" as version
Additional Info
This is the stuff you get from every slash command. In the example above I only use token ...
token=gIkuvaNzQIHg97ATvDxqgjtO
team_id=T0001
team_domain=example
channel_id=C2147483705
channel_name=test
user_id=U2147483697
user_name=Steve
command=/weather
text=94070
response_url=https://hooks.slack.com/commands/1234/5678
Works perfect for me, hope it helps :) Good luck !

Related

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

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

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.

HTTP Request to a function in Google Scripts

Since I'm not experienced at all with HTTP Request and Google Scripts, I'm having trouble wraping my head around it.
So, my problem is the following:
I'm currently trying to get information in my lua script and send it to a google Spreadsheet. However, the way the google spreadsheet should save the info would be dependent on which function on the Google Script I'm calling and passing information.
SO, my question is: How would my lua script (that only gives me access to HTTP Requests at this time) connect to a specific function like the one bellow?
function callName(name) {
// Get the last Row and add the name provided
var sheet = SpreadsheetApp.getActiveSheet();
sheet.getRange(sheet.getLastRow() + 1,1).setValue([name]);
}
Also, I think my script is wrong as well, but I'm more worried about how to actually make the connection.
Answer:
You can publish your script as a Web Application and use URL parameters to pass the script the information you need.
More Information:
From the Google documentation about web apps:
If you build a user interface for a script, you can publish the script as a web app. For example, a script that lets users schedule appointments with members of a support team would best be presented as a web app so that users can access it directly from their browsers.
However, even without building a user interface, you can use this functionality to run scripts on your sheet by utilising HTTP requests.
Modifying your Script:
In order to allow your script to accept URL parameters, you must first modify your code so that processing is done on a HTTP GET request. You can do this with the Apps Script doGet() function and the event parameter e:
function doGet(e) {
callName(e.parameter.name);
}
function callName(name) {
// Get the last Row and add the name provided
var sheet = SpreadsheetApp.getActiveSheet();
sheet.getRange(sheet.getLastRow() + 1,1).setValue([name]);
}
Setting up the Web App:
From the Apps Script user interface, follow the Publish > Deploy as web app... menu item, and in the newly-opened modal window, you'll want to select the following settings:
Project version: New
Execute the app as: Me (your-email#address.here)
Who has access to the app: Anyone, even anonymous
And click Deploy. Here, you will be given a URL in a new, smaller modal in the following form:
https://script.google.com/a/your-domain.com/macros/s/some-script-id/exec
Making the request:
The rest of this is now trivial - you can make your HTTP request to the script URL in the previous step, but providing the URL parameter that you need in order to give te app the information of the value you wish to set.
For example, if you want to set the value to the number 20, make your get request as so:
GET https://script.google.com/a/your-domain.com/macros/s/some-script-id/exec?name=20
Note the ?name=20 at the end gives the Web App the parameter name with a value of 20. The doGet(e) function reads this from e.parameter.name and sends it to your callName(name) function for processing and execution.
References:
Web Apps | Apps Script | Google Developers
Request Parameters

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.

receive parameters from another server POST request using apps script

I have some technical question about using apps script.
A third party server is sending me parameters in POST method and the request looks like this: https://script.google.com/macros/s/AKfycbyJYVLO46T1LnQKktaMrROclCOqgawcVfZaRbm_oXfJaMIYcPj8/exec?value=$postback_params_test$ (so I need to receive $postback_params_test$ as value)
I used doPost(e) function but with no success, I thing the problem is because apps script is based on client java script and it c'ant talk with server language, am I right? or there is an option to do it anyway through apps script?
my code:
function doPost(e){
var param = e.parameter.value;
var doc = SpreadsheetApp.openById("106jpepwZZWXtpO4Id45qmJovV68q_DIqpEmTQ0khf4E");
var cell = doc.getRange('a1');
cell.setValue(param);
}
8.6
Image added:
enter image description here
When deployed as a web app with settings "execute as: me" and "who has access to the app: anyone, even anonymous" your example code works.
Did you authorize the code? Before you can run it as a web app, you must run doPost() (or another function) once manually from within the script editor, so you can grant the appropriate permissions to the script.
If it's not the authorization issue, you can add a MailApp.sendemail() call to help you troubleshoot.
function doPost(e) {
MailApp.sendEmail('YOUR-EMAIL HERE','TEST doPost()',JSON.stringify(e));
This way you'll receive an email showing the raw request coming from the other server.
Be sure to re-run the script manually after adding the MailApp line so you can authorize it to send email, and update the published version.