Apologies if this is a silly question, I am very new to programming in general and I have never worked with OAuth before.
I am currently trying to build a Google App Script which interacts with an external service and authenticates using OAuth2.
I am having a lot of trouble with OAuth2... for starters, I'm not sure I am using the correct library for this. I am going off the one recommended by Google for ads script - https://developers.google.com/google-ads/scripts/docs/examples/oauth20-library
I know there is another popular one available in GitHub https://github.com/googleworkspace/apps-script-oauth2
I was not able to use it because everything comes out as 'is not a function', no matter how I add the library, manually or through the built-in feature.
I started building the API call, based on the first library and I had some partial success, I started getting back a 500 error message and I realized my accessToken is null.
function SHICall(){
var tokenUrl = "X/token";
const scriptProperties = PropertiesService.getScriptProperties();
var clientId = scriptProperties.getProperty('CLIENT_ID');
var clientSecret = scriptProperties.getProperty('SECRET');
var opt_scope = "CustomerAPI.Public";
// Access token is obtained and cached.
const authUrlFetch = OAuth2.withClientCredentials(tokenUrl, clientId, clientSecret, opt_scope);
const url = "X";
Logger.log(authUrlFetch);
var options = {
headers: { 'Content-Type': "application/json", 'Accept': "application/json"},
muteHttpExceptions: true,
method: "GET",
contentType: "application/json",
validateHttpsCertificates: false,
};
// Use access token in each request
const response = authUrlFetch.fetch(url, options);
// ... use response
Logger.log(response);
}
Any clue why the token is coming back as null? I based my API Call on google's documentation again https://developers.google.com/google-ads/scripts/docs/features/third-party-apis#oauth_2
Related
I've been trying to setup a small app using Google Apps Script that just posts to Instagram. I got the bulk of the code I'm using from this post.
function postToIG() {
var sheet = SpreadsheetApp.getActiveSpreadsheet().getSheetByName('IG Post');
var image = sheet.getRange(2,1).getValue();
var text = sheet.getRange(2,2).getValue();
var access_token = "TOKEN";
var formData = {
'image_url': image,
'caption': text,
'access_token': access_token
};
var options = {
'method' : 'post',
'payload' : formData,
"muteHttpExceptions": true
};
const container = 'https://graph.facebook.com/v15.0/ID/media';
const response = UrlFetchApp.fetch(container, options);
console.log(response.getContentText());
const creation = response.getContentText();
var data = JSON.parse(creation);
var creationId = data.id
var formDataPublish = {
'creation_id': creationId,
'access_token': access_token
};
var optionsPublish = {
'method' : 'post',
'payload' : formDataPublish,
"muteHttpExceptions": true
};
const sendinstagram = 'https://graph.facebook.com/v15.0/ID/media_publish';
var response2 = UrlFetchApp.fetch(sendinstagram, optionsPublish);
console.log(response2.getContentText());
}
I just keep getting the same error message for both requests in the code
{"error":{"message":"Unsupported post request. Object with ID
'ID' does not exist, cannot be loaded due to missing
permissions, or does not support this operation. Please read the Graph
API documentation at
https://developers.facebook.com/docs/graph-api","type":"GraphMethodException","code":100,"error_subcode":33,"fbtrace_id":"AtTVaV-ODec9Iuagp4efYuX"}}
I'm not sure how exactly to get the Instagram user ID that would go in for the ID value in the code. I've tried several different methods and gotten different IDs and I get the same error for all of them.
My account is a business account and linked to a Facebook page. Furthermore, I got the access token being used from the Graph API Explorer on the Facebook developers site. I added all of the permissions listed for the media and media_publish endpoints. Additionally, the Instagram Graph API product is added to the app I've created for this on the Facebook developers page.
EDIT:
Got what I think is the correct ID using the Graph API Explorer and accessing the endpoint "https://graph.facebook.com/v15.0/me/accounts" however even using this new ID value I am still getting the same error message as shown above
UPDATE
I was able to figure this out. I did have the correct ID value however it turns out there is also an Instagram Business Account ID that can be retrieved using the Graph API Explorer. Once I started using this ID, it ran just fine
client (imitating google sheets add-on) :
function getReqParam(type){
//DriveApp.getFiles() - commented row needed for the Drive scope request
let data = { param2: "value2" ,param1: Session.getActiveUser()};
var params = {
method: type,
contentType: "application/json",
headers: { Authorization: "Bearer " + ScriptApp.getOAuthToken() },
payload: JSON.stringify(data),
muteHttpExceptions: true,
};
return params;
}
function sendPostReq(){
let res = UrlFetchApp.fetch(url, getReqParam("POST"));
console.log("sendPostReq response: ", res.getResponseCode());
console.log("sendPostReq response: ", res.getContentText());
}
webapp:
function doPost(e){
console.log("doPost",e);
try{
saveRegToDB(JSON.parse(e.postData.contents));
e.method = "POST";
//return regisration status
e.regStatus = {status: "active"};
}catch(err){
e.regStatus = {status: "error"};
e.err = JSON.stringify(err);
e.errMSg = JSON.stringify(err.message);
console.error("doPost exception",err);
console.error("doPost exception",err.stack);
}
return ContentService.createTextOutput(JSON.stringify(e))
.setMimeType(ContentService.MimeType.JSON);
}
function saveRegToDB({param1,param2}={}){
let rowID = -1;
if (param1 && param2){
let ss = getSS();
let sheetName = "Registrations";
let s = ss.getSheetByName(sheetName);
let r = s.getLastRow() + 1;
rowID = r;
let rVals = [param1,param2,getTimeStamp()];
let vals = [rVals]; //2 dimensions array
console.log("reg saveToDB ",vals,r);
//write to sheet
s.getRange(r,1,1,rVals.length).setValues(vals);
}
console.log("saveRegToDB:" ,rowID);
return rowID;
}
function getSS(){
let ssid = getSSid();
let ss = SpreadsheetApp.openById(ssid);
SpreadsheetApp.setActiveSpreadsheet(ss);
return ss;
}
function getSSid(){
return PropertiesService.getScriptProperties()
.getProperty("regSSID");
}
function getTimeStamp() {
return Utilities.formatDate(new Date(), Session.getScriptTimeZone(), "yyyy-MM-dd HH:mm:ss");
}
I have a simple google web app. The web app is supposed to serve a google sheets addon, to store add-on related information to a private SS (owned by the add-on account), for example: users' registrations. I am wondering about the Who has access setting I should use in order to deploy the the web app, and the usage of access token that comes with it as explained here.
The Execute as (legacy editor: Execute the app as to Me
When deploying webapp with Who has access: Anyone with Google account (Anyone in legacy editor) web app should be called with access token
headers: { Authorization: "Bearer " + ScriptApp.getOAuthToken() }
There are 2 problems with that approach:
here it is claimed that webapp script project should be shared with all the users that might use it. In my case every user that will install the addon, that is: everyone. This is a disatvantage for itself. Despite that, another user (say, user2) can access successfully the web app.
In order the use access token the client has to request for Drive scopes, even though web app doesn't use any of the Drive API. More over, even of calling the web app with access code, it fails to access the user drive files, due to missing permissions Exception: You do not have permission to access the requested document.
So not only, client is enforced to ask for Drive scope web app does not need, even if trying to "use" Drive API to access client drive - it is blocked.
I have tested it by setting the regSSID script property read in getSSid() to spreadsheet owned by the user2, and also executing client from user2 account script editor
What is the benefit of deploying web app with Anyone with Google account (Anyone in legacy editor) and using access token for that?
When deploying webapp with Who has access: Anyone (Anyone, even anonymous in legacy editor) no access token is required, thus no need to ask for Drive scope. This is good thing. The question is what might the disadvantages of deploying the web app to Anyone, especially in terms of security. I guess web app can filtered out all calls from unwanted access by validating applicative post request parameter. Is this a real security issue?
I have a Google Sheets workspace addon and recently did some work to integrate BigQuery. Essentially BigQuery hold a record of books each of which has an author, title etc and my Addon allows people to pull the books that they have read into their sheet. The first column in the sheet allows people to choose from all the authors in the DB, based on that selection the second column is populated with data from BigQuery with all books by that author etc etc. There is no need for my AddOn to access a user's BigQuery, they only access 'my' BgQuery.
This all works fine, but when I submitted my addon for approval I was told
Unfortunately, we cannot approve your request for the use of the following scopes
https://www.googleapis.com/auth/bigquery
We recommend using service accounts for this type of information exchange.
This seems fair and reading up on Service Accounts it seems a much better fit for my use case. I've gone through the process of creating the service accounts and downloaded my security details json file, however I just can't figure out how to actually query BigQuery from AppScript.
In my non-service account method I have the BigQuery Library installed in AppScript and basically run
var queryResults = BigQuery.Jobs.query(request, projectId);
I've been trying to work from an example at https://developers.google.com/datastudio/solution/blocks/using-service-accounts
function getOauthService() {
var serviceAccountKey = getServiceAccountCreds('SERVICE_ACCOUNT_KEY');// from private_key not private_key_id of JSON file
var serviceAccountEmail = getServiceAccountCreds('SERVICE_ACCOUNT_EMAIL');
return OAuth2.createService('RowLevelSecurity')
.setAuthorizationBaseUrl('https://accounts.google.com/o/oauth2/auth')
.setTokenUrl('https://accounts.google.com/o/oauth2/token')
.setPrivateKey(serviceAccountKey)
.setIssuer(serviceAccountEmail)
.setPropertyStore(scriptProperties)
.setCache(CacheService.getScriptCache())
.setScope(['https://www.googleapis.com/auth/bigquery.readonly']);
}
function getData(request) {
var accessToken = getOauthService().getAccessToken();
var billingProjectId = getServiceAccountCreds('BILLING_PROJECT_ID');
// var email = Session.getEffectiveUser().getEmail();
// return cc
// .newBigQueryConfig()
// .setAccessToken(accessToken)
// .setBillingProjectId(billingProjectId)
// .setUseStandardSql(true)
// .setQuery(BASE_SQL)
// .addQueryParameter('email', bqTypes.STRING, email)
// .build();
}
I've commented out the code in the above which relates to
var cc = DataStudioApp.createCommunityConnector();
in the above tutorial since I'm not using DataStudio but I'm really not sure what to replace it with so I can query BigQuery with AppScript via a Service Account. Can anyone offer any advice?
Based on the advice from #TheAddonDepot in the comments above my revised code now looks like:
function getBigQueryService() {
return (
OAuth2.createService('BigQuery')
// Set the endpoint URL.
.setTokenUrl('https://accounts.google.com/o/oauth2/token')
// Set the private key and issuer.
.setPrivateKey(JSON_CREDS.private_key) // from the json file downloaded when you create service account
.setIssuer(JSON_CREDS.client_email). // from the json file downloaded when you create service account
// Set the property store where authorized tokens should be persisted.
.setPropertyStore(PropertiesService.getScriptProperties())
// Caching
.setCache(CacheService.getUserCache())
// Locking
.setLock(LockService.getUserLock())
// Set the scopes.
.setScope(['https://www.googleapis.com/auth/bigquery.readonly'])
// .setScope('https://www.googleapis.com/auth/bigquery')
)
}
function queryData(){
const bigQueryService = getBigQueryService()
if (!bigQueryService.hasAccess()) {
Logger.log("BQ ERROR IS "+ bigQueryService.getLastError())
}
//const projectId = bigqueryCredentials.project_id
var projectId = "<yourprojectid>"
let url = 'https://bigquery.googleapis.com/bigquery/v2/projects/<yourprojectid>/queries'; //projectID is taken from the security json file for the service account, although it doesn't seem to matter if you use the project code
const headers = {
Authorization: `Bearer ${bigQueryService.getAccessToken()}`,
'Content-Type': 'application/json',
}
var data = {query:"<your query>",useLegacySql:false};
const options = {
method: 'post',
headers,
//contentType: 'application/json',
payload: JSON.stringify(data),
muteHttpExceptions: true // on for debugging
}
try {
const response = UrlFetchApp.fetch(url, options)
const result = JSON.parse(response.getContentText())
Logger.log("here is result "+ JSON.stringify(result))
} catch (err) {
console.error(err)
}
}
I am not really familiar with the Telegram API and need to use JSON.
I want to create an inline keyboard that have options [yes/no] only.
Then, send it to a spreadsheet.
I managed to create the inline keyboard but nothing happens when I press it.
Is it possible to send data to a spreadsheet from the Telegram bot?
Code:
{"inline_keyboard":[[{"text":"yes","callback_data":"yes"}]]}
Yes, this is possible :)
You'd first need to setup a webhook for your telegram bot -
var telegramToken = 'Your-Telegram-Bot-Token-Goes-Here';
function setup() {
var method = 'setWebhook';
var payload = {
url: ScriptApp.getService().getUrl()
}
var options = {
'method' : 'post',
'contentType': 'application/json',
'payload' : JSON.stringify(payload)
};
var response = UrlFetchApp.fetch('https://api.telegram.org/bot' + telegramToken + '/' + method, options);
Logger.log(response);
}
and then setup a simple doPost(e) to capture incoming data from said webhook. You can refer some of the code from a bot that's i'd built here but please feel free to share more details with what you require so I could assist accordingly.
I tested it in the Google OAuth 2.0 Playground and it looked like I could return info from the site, but when I set up the OAuth2 code from Github, I can't seem to do a UrlFetchApp request as I get
the error returned code 403. Truncated server response: Not authorized to access this feed
I am not sure if this is because it is not enabled in the API console, but I can't find it there or under Advanced Google Services.
This is the section of code I am falling down at:
var service = getService();
if (service.hasAccess()) {
Logger.log("initial xml has access "service.hasAccess());
var headers = {
"Authorization": "Bearer " + service.getAccessToken()
};
var MyAttachmentsURL = 'https://sites.google.com/feeds/content/[DOMAIN]/[SITE NAME]?kind=attachment';
var response = UrlFetchApp.fetch(MyAttachmentsURL, headers);
};
The script from Github worked for me and I authorized when the message came up. This is what is in my scope tab:
7 OAuth Scopes required by the script:
https://sites.google.com/feeds
https://www.googleapis.com/auth/drive
https://www.googleapis.com/auth/script.container.ui
https://www.googleapis.com/auth/script.external_request
https://www.googleapis.com/auth/script.scriptapp
https://www.googleapis.com/auth/spreadsheets
https://www.googleapis.com/auth/userinfo.email
According to the Protocol Guide's "Authorizing requests with OAuth 2.0" You must activate the Google Sites API in the API Console if you can see that option (Step 2).
The only other issue I can see is the requiring to specify a version as GData-Version: 1.4.
So your code would change to something like this:
var service = getService();
if (service.hasAccess()) {
Logger.log("initial xml has access "service.hasAccess());
var headers = {
"GData-Version": "1.4",
"Authorization": "Bearer " + service.getAccessToken()
};
var MyAttachmentsURL = 'https://sites.google.com/feeds/content/[DOMAIN]/[SITE NAME]?kind=attachment';
var response = UrlFetchApp.fetch(MyAttachmentsURL, headers);
};
As long as the scope is mentioned in the code, it doesn't need to be passed, so that wasn't the issue. This was one of many variations I had been trying and I blame missing the post method on it being the wee hours. This code works (for now).
var service = getService();
if (service.hasAccess()) {
Logger.log("initial xml has access "+service.hasAccess());
var headers = {
// "GData-Version" : "1.4",
"Authorization" : "Bearer "+service.token_.access_token
};
var params = {"headers": headers, 'method':'get', 'muteHttpExceptions':true};
var MyAttachmentsURL = 'https://sites.google.com/feeds/content/[DOMAIN]/[SITE NAME]?kind=attachment';
var response = UrlFetchApp.fetch(MyAttachmentsURL, params);
};
It appears that "GData-Version" : "1.4" is returned in the response header so is not needed in the request. What is needed is the access token and although all the other API's seem to be able to make use of .getAccessToken, I had to amend this to .token_.access_token - this may be just for Google Sites.
I appreciate those who had a look at this and thank you Chris for responding.