muteHttpExceptions = true throws authentication failure - google-apps-script

Using the Google Apps Email Settings API a delegate is deleted with the script below. If an error occurs, for example trying to delete delegates that do not exist, the following message is returned:
Exception: Request failed for returned code 400. Truncated server
response: <xml version="1.0" encoding="UTF-8"?>
<AppsForYourDomainErrors> <error errorCode="1303"
invalidInput="XXXX#XXXX.com" reason="E... (use muteHttpExceptions
option to examine full response)
However when using muteHttpExceptions = true the authentication fails:
Exception: Failed to authenticate for service: google
This forces me to use a try / catch structure instead of examining the HTTPResponse object. I would like to know why this is happening and how to solve it.
The test function:
function test() {
var consumerKey = 'XXXX';
var consumerSecret = 'XXXX';
var domain = 'XXXX.com';
var userName = 'XXXX'
var delegateName = 'XXXX#XXXX.com'
var serviceName = 'google';
var scope = 'https://apps-apis.google.com/a/feeds/emailsettings/2.0/';
var oAuthConfig = UrlFetchApp.addOAuthService(serviceName);
oAuthConfig.setRequestTokenUrl('https://www.google.com/accounts/OAuthGetRequestToken?scope=' + scope);
oAuthConfig.setAuthorizationUrl('https://www.google.com/accounts/OAuthAuthorizeToken');
oAuthConfig.setAccessTokenUrl('https://www.google.com/accounts/OAuthGetAccessToken');
oAuthConfig.setConsumerKey(consumerKey);
oAuthConfig.setConsumerSecret(consumerSecret);
var fetchParameters = {};
fetchParameters.oAuthServiceName = serviceName;
fetchParameters.oAuthUseToken = 'always';
fetchParameters.method = 'DELETE';
fetchParameters.muteHttpExceptions = false;
try {
var url = 'https://apps-apis.google.com/a/feeds/emailsettings/2.0/'+ domain + '/' + userName + '/delegation/' + delegateName;
var result = UrlFetchApp.fetch(url, fetchParameters);
} catch (e) {
Logger.log(e);
}
}

This question has been posted to the Google Apps Script issue tracker as ticket 3478 and acknowledged as a bug. The ticket remains open but the following workaround has been proposed:
Revoking access in your Google Account's Security settings to both www.google.com AND the source of Apps Script (spreadsheet etc).
Change the oAuthServiceName parameter to something else.
Re-running the script

Related

Google Ads API with Google Apps Script no access token received

I am trying to use Google Ads on Google Apps Script using this guide. I created client id and client secret on Google Console's API & Services.
Not too sure if this configuration is correct but the account is linked to Google Apps Script as I have pagespeed insights as well and there are some requests on the dashboard. I added https://www.googleapis.com/auth/drive as the scope. Again not too sure if I should add Google Ads to the scope. Lastly, got my refresh token from Google Auth playground. When I run the script above I got the following error:
Error: No access token received: {
"error": "invalid_client",
"error_description": "Unauthorized"
}
authenticate_ # test.gs:120
withRefreshToken # test.gs:144
initializeOAuthClient # test.gs:28
Honestly not too sure what I am doing wrong here so any help would be very much appreciated. Thank you.
Edit
Codes:
//From Google Console API & Services
var CLIENT_ID = '"MY_CLIENT_ID';
var CLIENT_SECRET = 'MY_CLIENT_SECRET';
//From Google Authplayground
var REFRESH_TOKEN = 'REFRESH_TOKEN';
// Enter scopes which should match scopes in File > Project properties
// For this project, e.g.: https://www.googleapis.com/auth/drive
var SCOPES = "https://www.googleapis.com/auth/adwords";
// Script ID taken from 'File > Project Properties'
var SCRIPT_ID = 'MY_SCRIPT_ID';
var authUrlFetch;
// Call this function just once, to initialize the OAuth client.
function initializeOAuthClient() {
if (typeof OAuth2 === 'undefined') {
var libUrl = 'https://developers.google.com/google-ads/scripts/docs/examples/oauth20-library';
throw Error('OAuth2 library not found. Please take a copy of the OAuth2 ' +
'library from ' + libUrl + ' and append to the bottom of this script.');
}
var tokenUrl = 'https://accounts.google.com/o/oauth2/token';
authUrlFetch = OAuth2.withRefreshToken(tokenUrl, CLIENT_ID, CLIENT_SECRET,
REFRESH_TOKEN, SCOPES);
}
/**
* Execute a remote function.
* #param {string} remoteFunctionName The name of the function to execute.
* #param {Object[]} functionParams An array of JSON objects to pass to the
* remote function.
* #return {?Object} The return value from the function.
*/
function executeRemoteFunction(remoteFunctionName, functionParams) {
var apiParams = {
'function': remoteFunctionName,
'parameters': functionParams
};
var httpOptions = {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
payload: JSON.stringify(apiParams)
};
var url = 'https://script.googleapis.com/v1/scripts/' + SCRIPT_ID + ':run';
var response = authUrlFetch.fetch(url, httpOptions);
var data = JSON.parse(response.getContentText());
// Retrieve the value that has been returned from the execution.
if (data.error) {
throw Error('There was an error: ' + response.getContentText());
}
return data.response.result;
}
// Paste in OAuth2 library here, from:
// https://developers.google.com/google-ads/scripts/docs/examples/oauth20-library
I have pasted the oauth2.0 library under the codes above.
Edit 2
I fixed the part of function initializeOAuthClient. It now shows execution complete, but when I try to run function executeRemoteFunction I am getting TypeError: Cannot read property 'fetch' of undefined. I am guessing I have to input remoteFunctionName and functionParams but where do I find them?

How can I access my google apps script web apps?

I make a script to generate a form with data from my server, and deploy it as a web app. The script is in a google standard project, and is in develop status with specific user specified for testing. Proper scopes are indicated, and the user's credentials are successfully generated. Following the guide for "execute a function", however, I always got error: {"error": {"code": 404, "message": "Requested entity was not found.", "status": "NOT_FOUND"}}. Below are my codes:
in the code.js:
function doGet(e){
Logger.log('doGet is called');
var params = JSON.stringify(e);
Logger.log('params: '+params);
var mywork = params.parameters['mywork'];
var formName = params.parameter['formName'];
var subject = params.parameter['subject'];
var result = makeQuestionsForm(mywork, formName, subject);
return ContentService.createTextOutput(JSON.stringify(result))
.setMimeType(ContentService.MimeType.JSON);
}
function doPost(e){
Logger.log('doPost is called');
var params = JSON.stringify(e);
Logger.log('params: '+params);
var mywork = params.parameters['mywork'];
var formName = params.parameter['formName'];
var subject = params.parameter['subject'];
var result = makeQuestionsForm(mywork, formName, subject);
return ContentService.createTextOutput(JSON.stringify(result))
.setMimeType(ContentService.MimeType.JSON);
}
function makeQuestionsForm(mywork, formName, subject) {
// code for generating the form
}
My code calling the script:
service = build('script', 'v1', credentials=self.creds)
parameters = {'mywork':self.sheet_body,'subject':request.session['subject'],'formName':title}
api_request = {
"function": "doPost",
"parameters": parameters,
"devMode": True
}
response = service.scripts().run(body=api_request,
scriptId=SCRIPT_ID).execute()
I did many tries: specifying function as 'doPost', 'doGet', or 'makeQuestionsForm', specifying scriptId as the id of project or id of script. All got the "NOT_FOUND" result.
Alternatively, I tried urlopen method directly calling the script's url, with token provided at request header. Got not-authorized.
Can you help see, what I did wrong or missed? Thanks
This is a function that I use to access a webapp from a gmail addon:
function deleteBlackList() {
const url=Utilities.formatString('%s?mode=dable',burl());
const params={"muteHttpExceptions":true,"method":"get","headers": {"Authorization": "Bearer " + ScriptApp.getOAuthToken()}};
const resp=UrlFetchApp.fetch(url,params);
const ts=Utilities.formatDate(new Date(), "redacted", "E MMM dd,yyyy HH:mm:ss");
Logger.log(url);
Logger.log(resp.getContentText("UTF-8"));
var action=CardService.newAction().setFunctionName('buildEmailFilterUI');
return CardService.newCardBuilder()
.setHeader(CardService.newCardHeader().setTitle('Deleted BlackListed Emails')).addSection(CardService.newCardSection()
.addWidget(CardService.newTextParagraph().setText(Utilities.formatString('TimeStamp: %s\n%s\n',ts,resp.getContentText()))))
.addSection(CardService.newCardSection().addWidget(CardService.newTextButton().setText('Return to Opening Form').setOnClickAction(action)))
.build();
}
The burl is the exec version of the url and I just hardwire it into the addon.
I found the problem. To execute a function, the project has to be deployed as an API executable. It was deployed as web apps, that's why it's NOT FOUND. After deploy the project as an api executable, now, I can access the function. Permission denied though at the moment, but that's another issue.

Google App Script oAuth2 to Questrade API Invalid Token

I'm trying to used Google Apps Script to pull Questrade account information into a Google sheets spreadsheet. I've added the oAuth2 library from GitHub(https://github.com/googleworkspace/apps-script-oauth2) then mostly copy and pasted (with minor edits) from the example code.
The weird thing is this code has worked, exactly how it is, but a day later it no longer works and returns the following:
Exception: Request failed for https://api01.iq.questrade.com returned code 401. Truncated server response: {"code":1017,"message":"Access token is invalid"} (use muteHttpExceptions option to examine full response)
My Google Apps Script is posted below. I've only removed my Questrade Client_ID and Google Script Script_ID. I have three buttons in my spreadsheet which I've linked to functions in the script:
Button 1 - QT oAuth - calls showSidebar
Button 2 - Load Account Info - calls makeRequest
Button 3 - QT Logout - calls logout
Typically, I press the QT Logout button to reset 0Auth2 services then I press the QT oAuth button. This seems to successfully go through the authorization process. I then press the Load Account Info button and about 99 times out of 100 I get the invalid access token message. I don't know if it's relevant, but when I log into Questrades API hub I can see that the script is adding an authorization after the QT oAuth button is pressed but it seems to disappear after about a minute.
The script:
function getQTService() {
// Create a new service with the given name. The name will be used when
// persisting the authorized token, so ensure it is unique within the
// scope of the property store.
return OAuth2.createService('QT')
// Set the endpoint URLs.
.setAuthorizationBaseUrl('https://login.questrade.com/oauth2/authorize')
.setTokenUrl('https://login.questrade.com/oauth2/token')
// Set the client ID and secret.
.setClientId('Client_ID')
.setClientSecret(' ') //there is no client secret but oAuth2 requires one
// Set the name of the callback function in the script referenced
// above that should be invoked to complete the OAuth flow.
.setCallbackFunction('authCallback')
// Set the property store where authorized tokens should be persisted.
.setPropertyStore(PropertiesService.getUserProperties())
.setCache(CacheService.getUserCache())
// Set the scopes to request (space-separated for Google services).
.setScope('read_acc')
}
function showSidebar() {
var QTService = getQTService();
if (!QTService.hasAccess()) {
var authorizationUrl = QTService.getAuthorizationUrl();
var template = HtmlService.createTemplate(
'Authorize. ' +
'Reopen the sidebar when the authorization is complete.');
template.authorizationUrl = authorizationUrl;
var page = template.evaluate();
SpreadsheetApp.getUi().showSidebar(page);
} else {
// ...
}
}
function authCallback(request) {
var QTService = getQTService();
var isAuthorized = QTService.handleCallback(request);
if (isAuthorized) {
return HtmlService.createHtmlOutput('Success! You can close this tab.');
} else {
return HtmlService.createHtmlOutput('Denied. You can close this tab');
}
}
function makeRequest() {
var QTService = getQTService();
var token = QTService.getAccessToken();
var spreadsheet = SpreadsheetApp.openById("Script_ID");
// Get account number
var response = UrlFetchApp.fetch('https://api01.iq.questrade.com/v1/accounts',{
headers: {
Authorization: 'Bearer ' + token
}
});
var json = response.getContentText();
var accountdata = JSON.parse(json);
var j = 0;
while(j < accountdata.accounts.length) {
var Account_num = accountdata.accounts[j].number;
var Account_type = accountdata.accounts[j].type;
var sheet = spreadsheet.getSheetByName(Account_type);
// GET CASH BALANCE
var response = UrlFetchApp.fetch('https://api01.iq.questrade.com/v1/accounts/' + Account_num + '/balances',{
headers: {
Authorization: 'Bearer ' + token
}
});
json = response.getContentText();
var balancedata = JSON.parse(json);
var i = 0;
while(balancedata.perCurrencyBalances[i].currency != 'CAD') {
i=i+1;
}
//send cash value to spreadsheet
sheet.getRange("G1").setValue(balancedata.perCurrencyBalances[i].cash);
// GET POSITIONS
var response = UrlFetchApp.fetch('https://api01.iq.questrade.com/v1/accounts/' + Account_num + '/positions',{
headers: {
Authorization: 'Bearer ' + token
}
});
json = response.getContentText();
var positionsdata = JSON.parse(json);
var num_of_positions = positionsdata.positions.length;
var i = 0;
while(i < num_of_positions) { //this loop is not that smart assumes the positions are where I specify, fix later
if(positionsdata.positions[i].symbol == 'VCN.TO'){
sheet.getRange("D5").setValue(positionsdata.positions[i].openQuantity);
}
if(positionsdata.positions[i].symbol == 'VUN.TO') {
sheet.getRange("D6").setValue(positionsdata.positions[i].openQuantity);
}
if(positionsdata.positions[i].symbol == 'VIU.TO') {
sheet.getRange("D7").setValue(positionsdata.positions[i].openQuantity);
}
if(positionsdata.positions[i].symbol == 'VEE.TO') {
sheet.getRange("D8").setValue(positionsdata.positions[i].openQuantity);
}
i=i+1;
}
j=j+1;
}
//send cash value to spreadsheet
// sheet.getRange("G1").setValue(data.perCurrencyBalances[i].cash);
}
function logout() {
var service = getQTService();
service.reset();
}
Any advice on what might be going wrong here would be greatly appreciated.
I don't think you can rely on using api01. I think you have to extract the api_server from the auth call that gives you a token (or at least I did this using the example on https://www.questrade.com/api/documentation/getting-started with refresh_token). My last 3 refresh_token auths for a bearer have yielded the api06 endpoint. I took your code and with the oauth authorizing and using api06 it works fine.
The magic sauce is var api_server = QTService.getToken().api_server;

Is it possible to access a deployed Protected Google Web App via URL without logging in from browser each time?

I've deployed a protected web app, and I'd like to trigger it without logging in each time:
I'd like to access the web app URL without logging in:
Based on this document, it's not possible without logging in from browser:
https://github.com/tanaikech/taking-advantage-of-Web-Apps-with-google-apps-script/blob/master/README.md
If the script of Web Apps uses some scopes, client users have to
authorize the scopes by own browser.
I'm assuming scopes means the web app is protected.
I've tried this: https://github.com/gsuitedevs/apps-script-oauth2/blob/master/samples/GoogleServiceAccount.gs but it asks for "request access"
If I click on request access, then it shows me this:
At this point, I'm thinking it's not possible to setup a service account with scope to trigger a protected deployed web app without authenticating through a browser each time. Can anyone confirm this?
My assumption is that the web app scope is https://www.googleapis.com/auth/drive since it has access to all drive's files.
Update: (What I tried but didn't work)
I matched the scope from the script:
To the service account:
The blurred area above is the client id i got from:
I've generated the access token using this script:
function accessTokens(){
var private_key = "-----BEGIN PRIVATE KEY-----*****\n-----END PRIVATE KEY-----\n"; // private_key of JSON file retrieved by creating Service Account
var client_email = "****#****.iam.gserviceaccount.com"; // client_email of JSON file retrieved by creating Service Account
var scopes = ["https://www.googleapis.com/auth/documents","https://www.googleapis.com/auth/forms","https://www.googleapis.com/auth/script.external_request","https://www.googleapis.com/auth/spreadsheets","https://www.googleapis.com/auth/userinfo.email"]; // Scopes
var url = "https://www.googleapis.com/oauth2/v3/token";
var header = {
alg: "RS256",
typ: "JWT",
};
var now = Math.floor(Date.now() / 1000);
var claim = {
iss: client_email,
scope: scopes.join(" "),
aud: url,
exp: (now + 3600).toString(),
iat: now.toString(),
};
var signature = Utilities.base64Encode(JSON.stringify(header)) + "." + Utilities.base64Encode(JSON.stringify(claim));
var jwt = signature + "." + Utilities.base64Encode(Utilities.computeRsaSha256Signature(signature, private_key));
var params = {
method: "post",
payload: {
assertion: jwt,
grant_type: "urn:ietf:params:oauth:grant-type:jwt-bearer",
},
};
var res = UrlFetchApp.fetch(url, params).getContentText();
Logger.log(res);
var ss = SpreadsheetApp.getActiveSpreadsheet();
var sheet = ss.getSheetByName("Sheet1");
sheet.getRange(1, 3).setValue(JSON.parse(res)['access_token']);
}
And still has the same error, it asks for request access.
After a couple days into this, I've figured it out (with help of course).
Get the scope from your deployed web app script: File > Project Properties > Scopes
Add the scope along with https://www.googleapis.com/auth/drive in page Manage API client access https://admin.google.com/AdminHome?chromeless=1#OGX:ManageOauthClients (use comma delimited to add multiple scopes: http...,http..., etc.)
For the client name, get the client id from the service account page in your admin console: https://console.developers.google.com
Deploy your script Publish > Deploy as Web App
After generating access token(instruction below), append the access token with your deployed web app url &access_token=YOURTOKENHERE
Use this script with a google sheet, it will generate the access_token in cell A1 of Sheet1 (Replace the 4 variables with the info relevant to you):
function accessTokens(){
var private_key = "-----BEGIN PRIVATE KEY-----n-----END PRIVATE KEY-----\n"; // private_key of JSON file retrieved by creating Service Account
var client_email = "*****#****.iam.gserviceaccount.com"; // client_email of JSON file retrieved by creating Service Account
var scopes = ["https://www.googleapis.com/auth/documents","https://www.googleapis.com/auth/forms","https://www.googleapis.com/auth/script.external_request","https://www.googleapis.com/auth/spreadsheets","https://www.googleapis.com/auth/userinfo.email","https://www.googleapis.com/auth/drive"]; // Scopes
var impersonate_email = "" //impersonate email
var url = "https://www.googleapis.com/oauth2/v4/token";
var header = {
alg: "RS256",
typ: "JWT",
};
var now = Math.floor(Date.now() / 1000);
var claim = {
iss: client_email,
sub: impersonate_email,
scope: scopes.join(" "),
aud: url,
exp: (now + 3600).toString(),
iat: now.toString(),
};
var signature = Utilities.base64Encode(JSON.stringify(header)) + "." + Utilities.base64Encode(JSON.stringify(claim));
var jwt = signature + "." + Utilities.base64Encode(Utilities.computeRsaSha256Signature(signature, private_key));
var params = {
method: "post",
payload: {
assertion: jwt,
grant_type: "urn:ietf:params:oauth:grant-type:jwt-bearer",
},
};
var res = UrlFetchApp.fetch(url, params).getContentText();
Logger.log(res);
var ss = SpreadsheetApp.getActiveSpreadsheet();
var sheet = ss.getSheetByName("Sheet1");
sheet.getRange(1, 1).setValue(JSON.parse(res)['access_token']);
}

Edit user signature, but can't use Oauth

I'm tryng to edit the users signature via GAS.
I have found lots of examples and tutorials, and one of the Waqar Ahmad's answers looks very good - Email Settings APIs Authentication.
However, it doesn't work for me.
I don't really understand OAuth autorization, but couldn't find a tutorial for it.
With what should I replace anonymous?
oAuthConfig.setConsumerKey("anonymous");
oAuthConfig.setConsumerSecret("anonymous");
I go to https://console.developers.google.com/ create a project and use clientID for key and client secret for secret, is it right?
Adding more information:
This is the Waqar's code I'm using:
/*-----------------------------------------------------------------------------
This function will update the HTML signature of a user.
Input will be jason data
To disable signature, pass an empty string as signature value
sample parameter
ob = {user='hps', signature='<b>Regards</b><br>Waqar'}
To disable signature
ob = {user='hps', signature=''}
-----------------------------------------------------------------*/
function updateSignature(ob) {
//ob = {};
//ob.user = "hps";
//ob.signature = "<b>Regards</b><br>Waqar";
ob = {};
ob.user = "test#xxxx.it";
ob.signature = "<b>Regards</b><br>Waqar";
var base = 'https://apps-apis.google.com/a/feeds/emailsettings/2.0/';
var xmlRaw = '<?xml version="1.0" encoding="utf-8"?>'+
'<atom:entry xmlns:atom="http://www.w3.org/2005/Atom" xmlns:apps="http://schemas.google.com/apps/2006">'+
'<apps:property name="signature" value="'+htmlEncode(ob.signature)+'" />'+
'</atom:entry>';
var fetchArgs = googleOAuth_('emailSetting',base);
fetchArgs.method = 'PUT';
fetchArgs.payload = xmlRaw;
fetchArgs.contentType = 'application/atom+xml';
var domain = UserManager.getDomain();
var url = base+domain+'/'+ob.user+'/signature';
var urlFetch = UrlFetchApp.fetch(url, fetchArgs);
var status = urlFetch.getResponseCode();
return status;
}
//--------------------------------------------------------------------------
//This function will retreive Signature settings as json.
/*Sample returned object
{user=hps, signature=<b>Regards</b><br>Waqar}
*/
//-----------------------------------------------------------------
function retrieveSignature(user) {
var user = 'hps';
var base = 'https://apps-apis.google.com/a/feeds/emailsettings/2.0/';
var fetchArgs = googleOAuth_('emailSetting',base);
fetchArgs.method = 'GET';
var domain = UserManager.getDomain();
var url = base+domain+'/'+user+'/signature?alt=json';
var urlFetch = UrlFetchApp.fetch(url, fetchArgs);
var jsonString = urlFetch.getContentText();
var jsonArray = Utilities.jsonParse(jsonString).entry.apps$property;
var ob = {};
ob.user = user;
for(var i in jsonArray){
ob[jsonArray[i].name] = jsonArray[i].value;
}
return ob;
}
//Google oAuthConfig..
function googleOAuth_(name,scope) {
var oAuthConfig = UrlFetchApp.addOAuthService(name);
oAuthConfig.getAccessTokenUrl()
oAuthConfig.setRequestTokenUrl("https://www.google.com/accounts/OAuthGetRequestToken?scope="+scope);
oAuthConfig.setAuthorizationUrl("https://www.google.com/accounts/OAuthAuthorizeToken");
oAuthConfig.setAccessTokenUrl("https://www.google.com/accounts/OAuthGetAccessToken");
oAuthConfig.setConsumerKey("xxxxxxxxxxxx.apps.googleusercontent.com");
oAuthConfig.setConsumerSecret("xxxxxxxx-xxxxxx-xxxxx");
return {oAuthServiceName:name, oAuthUseToken:"always"};
}
//This function will escape '<' and '>' characters from a HTML string
function htmlEncode(str){
str = str.replace(/</g,'<');
return str.replace(/>/g,'>')
}
to get the oAuthConfig.setConsumerKey and oAuthConfig.setConsumerSecret I have created a new project in the google developer console, modified to on the Admin SDK API status, created a "Client ID for native application" and used the CLIENT ID in setConsuperKey, and CLIENT SECRET in setConsumerSecret.
Executing the script updateSignature the test#xxxx.it's signature should be changed,
I can see the box "autorization required" clik ok, and appear the request access box,
i click on "grant access" but nothing happes, and no error are shown.
Executing the same function in debug mode, i have the same boxes and a red box with "Errore OAuth" at the end.
I'm doing something wrong... please help me to find the mistake!!
Thanks again.
Marco
Finally i found the clue!!
Thank's to mike's quastions and answers
who let me discover This example
I Finally understand!!
in consumerkey you have to set your domain (p.e. "mydomain.it",
in consumerSecret you have to set the "Secret data of the customer to OAuth:" from admin google console->secority->advanced settings->Manage data OAuth key and secret for this domain.
I was confusing by the secret key in the google developer console, may be this answer can help