Setting Oauth2 to get access token using spreadsheets - google-apps-script

I've been using Spreadsheets and upwork, as I want to integrate between them.
So I'm trying to make authorization for upwork through spreadsheets, using the documentation steps and everything is going fine. But when i authorize my account i see that the response_type is not a token, it's a code.
function authorize() {
return OAuth2.createService('upwork')
// Set the endpoint URLs, which are the same for all Google services.
.setAuthorizationBaseUrl('https://www.upwork.com/ab/account-security/oauth2/authorize')
.setTokenUrl('https://www.upwork.com/ab/account-security/oauth2/token')
// Set the client ID and secret, from the Google Developers Console.
.setClientId('cleintID')
.setClientSecret('clientSecret')
.setRedirectUri('https://script.google.com/macros/d/{Script-ID}/usercallback')
.setTokenFormat(OAuth2.TOKEN_FORMAT.FORM_URL_ENCODED)
// 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())
}
So this is my first question, Is there is a way to set the response_type to token, as i am setting in the picture above the clientID,redirectUri... (I tried setResponseType but it doesn't exist)
although working as it's and using response_type=code, after authorization the a request should be returned to the
function as below function authCallback(request) {
var driveService = authorize();
Logger.log(request) //this is always null
var isAuthorized = driveService.handleCallback(request);
if (isAuthorized) {
Logger.log('Yaaay')
return HtmlService.createHtmlOutput('Success! You can close this tab.');
} else {
Logger.log('Naaay')
return HtmlService.createHtmlOutput('Denied. You can close this tab');
}
}
authorize() is not called ??
Finally below is the whole code i'm using.
function authorize() {
return OAuth2.createService('upwork')
// Set the endpoint URLs, which are the same for all Google services.
.setAuthorizationBaseUrl('https://www.upwork.com/ab/account-security/oauth2/authorize')
.setTokenUrl('https://www.upwork.com/ab/account-security/oauth2/token')
// Set the client ID and secret, from the Google Developers Console.
.setClientId('cleintID')
.setClientSecret('clientSecret')
.setRedirectUri('https://script.google.com/macros/d/{Script-ID}/usercallback')
.setTokenFormat(OAuth2.TOKEN_FORMAT.FORM_URL_ENCODED)
// 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())
}
function redirect() {
var driveService = authorize();
if (!driveService.hasAccess()) {
var authorizationUrl = driveService.getAuthorizationUrl();
var template = HtmlService.createTemplate(
'Press here to authroze.');
template.authorizationUrl = authorizationUrl;
var page = template.evaluate().setTitle('Authroize');
SpreadsheetApp.getUi().showSidebar(page);
}
}
function authCallback(request) {
var driveService = authorize();
Logger.log(request)
var isAuthorized = driveService.handleCallback(request);
if (isAuthorized) {
Logger.log('Yaaay')
return HtmlService.createHtmlOutput('Success! You can close this tab.');
} else {
Logger.log('Naaay')
return HtmlService.createHtmlOutput('Denied. You can close this tab');
}
}
Update: I found that redirectURI is incorrect format, as google require the URI to be in the form of https://script.google.com/macros/d/{SCRIPT ID}/usercallback, so now the error changes and i got a failure
Error: Error retrieving token: 404: {"<!DOCTYPE html>\n<html lang":"\"en\">\n<head>\n\n <meta name","":"undefined"," window.performance.mark
":"undefined"," window.performance.mark(name);\n }\n\n function remove_mark(name) {\n window.performance ":"undefined"," window.performance.clearMarks ":"undefined"," window.performance.clearMarks(name);\n }\n\n function
create_measure(name, startMark, endMark) {\n window.performance ":"undefined"," window.performance.measure ":"undefined"," window.performance.measure(name, startMark, endMark);\n }\n\n function remove_measure(name) {\n window.performance ":"undefined"," window.performance.clearMeasures ":"undefined"," window.performance.clearMeasures(name);\n }\n</script>\n <link
rel":"\"stylesheet\" href","amp;site.application":"AccountSecurity","amp;site.version":"ed46c62af44692608c290dd0b498fa933b20c567","amp;site.environment":"prod","amp;server-error.status":"1",
"amp;server-error.label":"404+-+Agate","amp;server-error.traceId":"5c7d08790a6473b1-IAD\" height","l":"'+l:'';j.async","granted_time":1598271686} (line 541, file "Service")

.setTokenUrl('https://www.upwork.com/ab/account-security/oauth2/token')
According to documentation, tokenurl should be
POST /api/v3/oauth2/token
Use:
.setTokenUrl('https://www.upwork.com/api/v3/oauth2/token')
406 is due to incorrect or no Accept headers. This header is set due to
.setTokenFormat(OAuth2.TOKEN_FORMAT.FORM_URL_ENCODED)
Remove it to use default accept headers:
//.setTokenFormat(OAuth2.TOKEN_FORMAT.FORM_URL_ENCODED)

Related

Executing Functions using the Apps Script API

I am trying to execute Apps Script function using Apps Script API. For this I set up a target script and calling script (JavaScript code) using instructions from Google here.
I followed exactly how it is described but I am getting the following errors.
Error in calling script:
ReferenceError: gapi is not defined
Error on target script while manually running the function "getFoldersUnderRoot()"
Exception: We're sorry, a server error occurred. Please wait a bit and try again.
function "getFoldersUnderRoot()" was running properly before connecting the target script to the GCP project.
Any help is appreciated to point out what I am doing wrong.
I figured out how to execute App Script functions using Apps Script API. So I am posting the answer for the benefit of others. Also I would try to plug in the missing information which Google has not provided in their instructions.
The target script is an App Script (e.g. "code.gs") file with the required functions to be executed. This script has to be attached to a GCP project with App Script API enabled.
The calling script has to be an html file saved in a local folder and not an App Script file. Below is an example "index.html" which calls two functions "callScriptFunction()" and "getSheets()".
<!DOCTYPE html>
<html>
<head>
<title>Google Apps Script API Quickstart</title>
<meta charset="utf-8" />
</head>
<body>
<p>Google Apps Script API Quickstart</p>
<!--Add buttons to initiate auth sequence and sign out-->
<button id="authorize_button" style="display: none;">Authorize</button>
<button id="signout_button" style="display: none;">Sign Out</button>
<pre id="content" style="white-space: pre-wrap;"></pre>
<script type="text/javascript">
// Client ID and API key from the Developer Console
var CLIENT_ID = 'YOUR_CLIENT_ID';
var API_KEY = 'YOUR_API_KEY';
// Array of API discovery doc URLs for APIs used by the quickstart
var DISCOVERY_DOCS = ["https://script.googleapis.com/$discovery/rest?version=v1"];
// Authorization scopes required by the API; multiple scopes can be
// included, separated by spaces.
var SCOPES = 'https://www.googleapis.com/auth/spreadsheets https://www.googleapis.com/auth/drive.readonly';
var authorizeButton = document.getElementById('authorize_button');
var signoutButton = document.getElementById('signout_button');
/**
* On load, called to load the auth2 library and API client library.
*/
function handleClientLoad() {
gapi.load('client:auth2', initClient);
}
/**
* Initializes the API client library and sets up sign-in state
* listeners.
*/
function initClient() {
gapi.client.init({
apiKey: API_KEY,
clientId: CLIENT_ID,
discoveryDocs: DISCOVERY_DOCS,
scope: SCOPES
}).then(function () {
// Listen for sign-in state changes.
gapi.auth2.getAuthInstance().isSignedIn.listen(updateSigninStatus);
// Handle the initial sign-in state.
updateSigninStatus(gapi.auth2.getAuthInstance().isSignedIn.get());
authorizeButton.onclick = handleAuthClick;
signoutButton.onclick = handleSignoutClick;
}, function(error) {
appendPre(JSON.stringify(error, null, 2));
});
}
/**
* Called when the signed in status changes, to update the UI
* appropriately. After a sign-in, the API is called.
*/
function updateSigninStatus(isSignedIn) {
if (isSignedIn) {
authorizeButton.style.display = 'none';
signoutButton.style.display = 'block';
// callScriptFunction();
getSheets();
} else {
authorizeButton.style.display = 'block';
signoutButton.style.display = 'none';
}
}
/**
* Sign in the user upon button click.
*/
function handleAuthClick(event) {
gapi.auth2.getAuthInstance().signIn();
}
/**
* Sign out the user upon button click.
*/
function handleSignoutClick(event) {
gapi.auth2.getAuthInstance().signOut();
}
/**
* Append a pre element to the body containing the given message
* as its text node. Used to display the results of the API call.
*
* #param {string} message Text to be placed in pre element.
*/
function appendPre(message) {
var pre = document.getElementById('content');
var textContent = document.createTextNode(message + '\n');
pre.appendChild(textContent);
}
/**
* Shows basic usage of the Apps Script API.
*
* Call the Apps Script API to create a new script project, upload files
* to the project, and log the script's URL to the user.
*/
function callScriptFunction() {
var scriptId = "TARGET_SCRIPT_ID";
// Call the Apps Script API run method
// 'scriptId' is the URL parameter that states what script to run
// 'resource' describes the run request body (with the function name
// to execute)
gapi.client.script.scripts.run({
'scriptId': scriptId,
'resource': {
'function': 'getFoldersUnderRoot',
'devMode': true
}
}).then(function(resp) {
var result = resp.result;
if (result.error && result.error.status) {
// The API encountered a problem before the script
// started executing.
appendPre('Error calling API:');
appendPre(JSON.stringify(result, null, 2));
} else if (result.error) {
// The API executed, but the script returned an error.
// Extract the first (and only) set of error details.
// The values of this object are the script's 'errorMessage' and
// 'errorType', and an array of stack trace elements.
var error = result.error.details[0];
appendPre('Script error message: ' + error.errorMessage);
if (error.scriptStackTraceElements) {
// There may not be a stacktrace if the script didn't start
// executing.
appendPre('Script error stacktrace:');
for (var i = 0; i < error.scriptStackTraceElements.length; i++) {
var trace = error.scriptStackTraceElements[i];
appendPre('\t' + trace.function + ':' + trace.lineNumber);
}
}
} else {
// The structure of the result will depend upon what the Apps
// Script function returns. Here, the function returns an Apps
// Script Object with String keys and values, and so the result
// is treated as a JavaScript object (folderSet).
var folderSet = result.response.result;
if (Object.keys(folderSet).length == 0) {
appendPre('No folders returned!');
} else {
appendPre('Folders under your root folder:');
Object.keys(folderSet).forEach(function(id){
appendPre('\t' + folderSet[id] + ' (' + id + ')');
});
}
}
});
}
function getSheets() {
// ID of the script to call. Acquire this from the Apps Script editor,
// under Publish > Deploy as API executable.
var scriptId = "TARGET_SCRIPT_ID";
// Initialize parameters for function call.
var sheetId = "SPREADSHEET_ID";
gapi.client.script.scripts.run({
'scriptId': scriptId,
'resource': {
'function': 'getSheetNames',
'parameters': [sheetId],
'devMode': true
}
}).then(function(resp) {
var result = resp.result;
if (result.error && result.error.status) {
// The API encountered a problem before the script
// started executing.
appendPre('Error calling API:');
appendPre(JSON.stringify(result, null, 2));
} else if (result.error) {
// The API executed, but the script returned an error.
// Extract the first (and only) set of error details.
// The values of this object are the script's 'errorMessage' and
// 'errorType', and an array of stack trace elements.
var error = result.error.details[0];
appendPre('Script error message: ' + error.errorMessage);
if (error.scriptStackTraceElements) {
// There may not be a stacktrace if the script didn't start
// executing.
appendPre('Script error stacktrace:');
for (var i = 0; i < error.scriptStackTraceElements.length; i++) {
var trace = error.scriptStackTraceElements[i];
appendPre('\t' + trace.function + ':' + trace.lineNumber);
}
}
} else {
// The structure of the result will depend upon what the Apps
// Script function returns. Here, the function returns an Apps
// Script Object with String keys and values, and so the result
// is treated as a JavaScript object (folderSet).
var names = result.response.result;
if (Object.keys(names).length == 0) {
appendPre('No sheetnames returned!');
} else {
appendPre(names);
}
}
});
}
</script>
<script async defer src="https://apis.google.com/js/api.js"
onload="this.onload=function(){};handleClientLoad()"
onreadystatechange="if (this.readyState === 'complete') this.onload()">
</script>
</body>
</html>
Below is an example of target script.
function getFoldersUnderRoot() {
var root = DriveApp.getRootFolder();
var folders = root.getFolders();
var folderSet = {};
while (folders.hasNext()) {
var folder = folders.next();
folderSet[folder.getId()] = folder.getName();
}
return folderSet;
}
function getSheetNames(sheetId) {
var ss = SpreadsheetApp.openById(sheetId);
var sheets = ss.getSheets();
var names = sheets.map(function(sheet) {
return sheet.getName();
})
return names;
}
From terminal change to the working directory and execute python3 -m http.server 8000. Open browser and load "http://localhost:8000/". Authourize and proceed.
You need to whitelist "http://localhost:8000/" in the project credentials
You need to add required scopes in OAuth Consent Screen of the project.
I am able to execute the function "getSheetNames()" but "getFoldersUnderRoot()" is throwing error: Exception: We're sorry, a server error occurred. Please wait a bit and try again. Executing from the script editor also gives the same error. However "getFoldersUnderRoot()" is executable on any other script which is not attached to a GCP project

Having issues with Twitter API authorization in Google Apps Script

I've used the documentation posted here https://github.com/gsuitedevs/apps-script-oauth1
I am having issues getting the function to authorize. I am new to working with API's so please bear with me. Trying to do a simple get request from twitter but the first part isn't going through. Any idea where things are going wrong? *Note ive loaded the Oauth1 library.
function getTwitterService() {
// 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.
var service = OAuth1.createService('twitter')
// Set the endpoint URLs.
service.setAccessTokenUrl('https://api.twitter.com/oauth/access_token')
service.setRequestTokenUrl('https://api.twitter.com/oauth/request_token')
service.setAuthorizationUrl('https://api.twitter.com/oauth/authorize')
// Set the consumer key and secret.
service.setConsumerKey('myKey')
service.setConsumerSecret('mySecret')
// 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());
function authCallback(request) {
var twitterService = getTwitterService();
var isAuthorized = twitterService.handleCallback(request);
if (isAuthorized) {
return Logger.log('Success! You can close this tab.');
} else {
return Logger.log('Denied. You can close this tab');
}
}
function makeRequest() {
Logger.log(authorizationUrl);
var twitterService = getTwitterService();
var response = twitterService.fetch("https://api.twitter.com/1.1/followers/list.json?screen_name='xyz'");
var post = response.getContentText();
Logger.log(post);
}
}
The getTwitterService() method should return the service object.
Currently, all the other methods in the snippet are declared inside the getTwitterService method.
function getTwitterService() {
return OAuth1.createService('twitter')
.setAccessTokenUrl('https://api.twitter.com/oauth/access_token')
.setRequestTokenUrl('https://api.twitter.com/oauth/request_token')
.setAuthorizationUrl('https://api.twitter.com/oauth/authorize')
.setConsumerKey('myKey')
.setConsumerSecret('mySecret')
.setCallbackFunction('authCallback')
.setPropertyStore(PropertiesService.getUserProperties());
}

Check if user has run it

I run a Google Apps script that uploads a file to the user's Google Drive file:
function doGet(e) {
var blob = UrlFetchApp.fetch(e.parameters.url).getBlob();
DriveApp.createFile(blob);
return HtmlService.createHtmlOutput("DONE!");
}
My site loads a popup window that runs a Google Apps Script with that code. Works fine.
Now, how do I communicate back to my site that they user has successfully uploaded the file? As in, how can I communicate back to my server that the user has run doGet()?`
Some type of response handling must exist?
Full working code (test it out on JSBin):
<!DOCTYPE html>
<html>
<head>
<script src="https://ajax.googleapis.com/ajax/libs/jquery/1.12.4/jquery.js"></script>
</head>
<body>
<div class="google-upload" data-url="https://calibre-ebook.com/downloads/demos/demo.docx">
<span style="background-color: #ddd">Upload</span>
</div>
<script>
$(function() {
$(".google-upload").click(function() {
var url = "https://script.google.com/macros/s/AKfycbwsuIcO5R86Xgv4E1k1ZtgtfKaENaKq2ZfsLGWZ4aqR0d9WBYc/exec"; // Please input the URL here.
var withQuery = url + "?url=";
window.open(withQuery + $('.google-upload').attr("data-url"), "_blank", "width=600,height=600,scrollbars=1");
});
});
</script>
</body>
</html>
So to clarify, I want a way to find out whether if the user has successfully uploaded the file. Something like:
request.execute(function(response) {
if (response.code == 'uploaded') {
// uploaded, do stuff
} else {
// you get the idea...
}
});
Adding a bounty for a complete solution to this.
Rather than returning a HtmlService object, you can pass data using jQuery's $.getJSON method and retrieve data from the doGet function with ContentService. Google Apps Script does not accept CORS, so using JSONP is the best way to get data to and from your script. See this post for more.
Working CodePen Example
I split your HTML and scripts for clarity. None of the HTML changed from your original example.
Code.gs
function doGet(e) {
var returnValue;
// Set the callback param. See https://stackoverflow.com/questions/29525860/
var callback = e.parameter.callback;
// Get the file and create it in Drive
try {
var blob = UrlFetchApp.fetch(e.parameters.url).getBlob();
DriveApp.createFile(blob);
// If successful, return okay
// Structure this JSON however you want. Parsing happens on the client side.
returnValue = {status: 'okay'};
} catch(e) {
Logger.log(e);
// If a failure, return error message to the client
returnValue = {status: e.message}
}
// Returning as JSONP allows for crossorigin requests
return ContentService.createTextOutput(callback +'(' + JSON.stringify(returnValue) + ')').setMimeType(ContentService.MimeType.JAVASCRIPT);
}
Client JS
$(function() {
$(".google-upload").click(function() {
var appUrl = "https://script.google.com/macros/s/AKfycbyUvgKdhubzlpYmO3Marv7iFOZwJNJZaZrFTXCksxtl2kqW7vg/exec";
var query = appUrl + "?url=";
var popupUrl = query + $('.google-upload').attr("data-url") + "&callback=?";
console.log(popupUrl)
// Open this to start authentication.
// If already authenticated, the window will close on its own.
var popup = window.open(popupUrl, "_blank", "width=600,height=600,scrollbars=1");
$.getJSON(popupUrl, function(returnValue) {
// Log the value from the script
console.log(returnValue.status);
if(returnValue.status == "okay") {
// Do stuff, like notify the user, close the window
popup.close();
$("#result").html("Document successfully uploaded");
} else {
$("#result").html(returnValue);
}
})
});
});
You can test the error message by passing an empty string in the data-url param. The message is returned in the console as well as the page for the user.
Edit 3.7.18
The above solution has problems with controlling the authorization flow. After researching and speaking with a Drive engineer (see thread here) I've reworked this into a self-hosted example based on the Apps Script API and running the project as an API executable rather than an Apps Script Web App. This will allow you to access the [run](https://developers.google.com/apps-script/api/reference/rest/v1/scripts/run) method outside an Apps Script web app.
Setup
Follow the Google Apps Script API instructions for JavaScript. The Apps Script project should be a standalone (not linked to a document) and published as API executable. You'll need to open the Cloud Console and create OAuth credentials and an API key.
The instructions have you use a Python server on your computer. I use the Node JS server, http-server, but you can also put it live online and test from there. You'll need to whitelist your source in the Cloud Console.
The client
Since this is self hosted, you'll need a plain HTML page which authorizes the user through the OAuth2 API via JavaScript. This is preferrable because it keeps the user signed in, allowing for multiple API calls to your script without reauthorization. The code below works for this application and uses the authorization flow from the Google quickstart guides.
index.html
<body>
<!--Add buttons to initiate auth sequence and sign out-->
<button id="authorize-button" style="display: none;">Authorize</button>
<button id="signout-button" style="display: none;">Sign Out</button>
<button onclick="uploadDoc()" style="margin: 10px;" id="google-upload" data-url="https://calibre-ebook.com/downloads/demos/demo.docx">Upload doc</button>
<pre id="content"></pre>
</body>
index.js
// Client ID and API key from the Developer Console
var CLIENT_ID = 'YOUR_CLIENT_ID';
var API_KEY = 'YOUR_API_KEY';
var SCRIPT_ID = 'YOUR_SCRIPT_ID';
// Array of API discovery doc URLs for APIs used by the quickstart
var DISCOVERY_DOCS = ["https://script.googleapis.com/$discovery/rest?version=v1"];
// Authorization scopes required by the API; multiple scopes can be
// included, separated by spaces.
var SCOPES = 'https://www.googleapis.com/auth/drive https://www.googleapis.com/auth/script.external_request';
var authorizeButton = document.getElementById('authorize-button');
var signoutButton = document.getElementById('signout-button');
var uploadButton = document.getElementById('google-upload');
var docUrl = uploadButton.getAttribute('data-url').value;
// Set the global variable for user authentication
var isAuth = false;
/**
* On load, called to load the auth2 library and API client library.
*/
function handleClientLoad() {
gapi.load('client:auth2', initClient);
}
/**
* Initializes the API client library and sets up sign-in state
* listeners.
*/
function initClient() {
gapi.client.init({
apiKey: API_KEY,
clientId: CLIENT_ID,
discoveryDocs: DISCOVERY_DOCS,
scope: SCOPES
}).then(function () {
// Listen for sign-in state changes.
gapi.auth2.getAuthInstance().isSignedIn.listen(updateSigninStatus);
// Handle the initial sign-in state.
updateSigninStatus(gapi.auth2.getAuthInstance().isSignedIn.get());
authorizeButton.onclick = handleAuthClick;
signoutButton.onclick = handleSignoutClick;
// uploadButton.onclick = uploadDoc;
});
}
/**
* Called when the Upload button is clicked. Reset the
* global variable to `true` and upload the document.
* Thanks to #JackBrown for the logic.
*/
function updateSigninStatus(isSignedIn) {
if (isSignedIn && !isAuth) {
authorizeButton.style.display = 'none';
signoutButton.style.display = 'block';
uploadButton.style.display = 'block'
uploadButton.onclick = uploadDoc;
} else if (isSignedIn && isAuth) {
authorizeButton.style.display = 'none';
signoutButton.style.display = 'block';
uploadButton.style.display = 'block';
uploadDoc();
} else {
authorizeButton.style.display = 'block';
signoutButton.style.display = 'none';
uploadButton.style.display = 'none';
isAuth = false;
}
}
/**
* Sign in the user upon button click.
*/
function handleAuthClick(event) {
gapi.auth2.getAuthInstance().signIn();
isAuth = true; // Update the global variable
}
/**
* Sign out the user upon button click.
*/
function handleSignoutClick(event) {
gapi.auth2.getAuthInstance().signOut();
isAuth = false; // update the global variable
}
/**
* Append a pre element to the body containing the given message
* as its text node. Used to display the results of the API call.
*
* #param {string} message Text to be placed in pre element.
*/
function appendPre(message) {
var pre = document.getElementById('content');
var textContent = document.createTextNode(message + '\n');
pre.appendChild(textContent);
}
/**
* Handle the login if signed out, return a Promise
* to call the upload Docs function after signin.
**/
function uploadDoc() {
console.log("clicked!")
var docUrl = document.getElementById('google-upload').getAttribute('data-url');
gapi.client.script.scripts.run({
'scriptId':SCRIPT_ID,
'function':'uploadDoc',
'parameters': [ docUrl ]
}).then(function(resp) {
var result = resp.result;
if(result.error && result.error.status) {
// Error before the script was Called
appendPre('Error calling API');
appendPre(JSON.parse(result, null, 2));
} else if(result.error) {
// The API executed, but the script returned an error.
// Extract the first (and only) set of error details.
// The values of this object are the script's 'errorMessage' and
// 'errorType', and an array of stack trace elements.
var error = result.error.details[0];
appendPre('Script error message: ' + error.errorMessage);
if (error.scriptStackTraceElements) {
// There may not be a stacktrace if the script didn't start
// executing.
appendPre('Script error stacktrace:');
for (var i = 0; i < error.scriptStackTraceElements.length; i++) {
var trace = error.scriptStackTraceElements[i];
appendPre('\t' + trace.function + ':' + trace.lineNumber);
}
}
} else {
// The structure of the result will depend upon what the Apps
// Script function returns. Here, the function returns an Apps
// Script Object with String keys and values, and so the result
// is treated as a JavaScript object (folderSet).
console.log(resp.result)
var msg = resp.result.response.result;
appendPre(msg);
// do more stuff with the response code
}
})
}
Apps Script
The Apps Script code does not need to be modified much. Instead of returning using ContentService, we can return plain JSON objects to be used by the client.
function uploadDoc(e) {
Logger.log(e);
var returnValue = {};
// Set the callback URL. See https://stackoverflow.com/questions/29525860/
Logger.log("Uploading the document...");
try {
// Get the file and create it in Drive
var blob = UrlFetchApp.fetch(e).getBlob();
DriveApp.createFile(blob);
// If successful, return okay
var msg = "The document was successfully uploaded!";
return msg;
} catch(e) {
Logger.log(e);
// If a failure, return error message to the client
return e.message
}
}
I had a hard time getting CodePen whitelisted, so I have an example hosted securely on my own site using the code above. Feel free to inspect the source and take a look at the live Apps Script project.
Note that the user will need to reauthorize as you add or change scopes in your Apps Script project.

OAuth2 with Google Cloud Print

I had written a Google Apps Script that connected to Google Cloud Print to automate some printing. The script would auto-run on a time interval, search for relevant files, and if found it would sent them to my printer. My code used OAuthConfig and was working fine, but now that class has been deprecated and after a weekend of trial & error and scouring the interwebs I can't get it to work with OAuth2.
Here's the OAuthConfig code that was working fine:
function printDoc(docId, docTitle, myPrinterId) {
var scope = 'https://www.googleapis.com/auth/cloudprint';
var url = 'https://www.google.com/cloudprint/submit';
var payloadOfSubmit = {
"printerid" : myPrinterId,
"title" : docTitle,
"content" : docId,
"contentType" : "google.kix"
};
var fetchArgs = googleOAuth_('google', scope, payloadOfSubmit);
fetchArgs.method = 'POST';
var responseOfSubmit = UrlFetchApp.fetch(url, fetchArgs);
var jsonOfSubmit = JSON.parse(responseOfSubmit.getContentText());
return jsonOfSubmit;
}
function googleOAuth_(name, scope, payloadData) {
var oAuthConfig = UrlFetchApp.addOAuthService(name);
oAuthConfig.setAuthorizationUrl("https://www.google.com/accounts/OAuthAuthorizeToken");
oAuthConfig.setRequestTokenUrl("https://www.google.com/accounts/OAuthGetRequestToken?scope="+scope);
oAuthConfig.setAccessTokenUrl("https://www.google.com/accounts/OAuthGetAccessToken");
oAuthConfig.setConsumerKey("anonymous");
oAuthConfig.setConsumerSecret("anonymous");
return {
oAuthServiceName:name,
oAuthUseToken:"always",
muteHttpExceptions:true,
payload:payloadData
};
}
I've successfully connected the github library for OAuth2. However, what's different about the instructions provided there, and on many other sites, is that they assume that the code will be deployed as a web service where a user is prompted to manually click to authorize the request. In my case the code will be saved on a Google Apps Script file, and the Cloud Printer is on the same Google account, so I never needed this manual intervention or back & forth with my original OAuthconfig.
My first attempt by adapting the instructions was:
function printDoc2(docId, docTitle, myPrinterId) {
var url = 'https://www.google.com/cloudprint/submit';
var scope = 'https://www.googleapis.com/auth/cloudprint';
var payloadOfSubmit = {
"printerid" : myPrinterId,
"title" : docTitle,
"content" : docId,
"contentType" : "google.kix",
};
var accessToken = googleOAuth_('google', scope).getAccessToken();
var params = {
method:"POST",
headers: {"Authorization": "Bearer " + accessToken},
muteHttpExceptions:true,
payload:payloadOfSubmit
};
var responseOfSubmit = UrlFetchApp.fetch(url, params);
//Logger.log(responseOfSubmit);
var jsonOfSubmit = JSON.parse(responseOfSubmit.getContentText());
return jsonOfSubmit;
}
function googleOAuth2_(name, scope) {
return OAuth2.createService(name)
.setAuthorizationBaseUrl('https://accounts.google.com/o/oauth2/auth')
.setTokenUrl('https://accounts.google.com/o/oauth2/token')
.setClientId("anonymous")
.setClientSecret("anonymous")
.setProjectKey(ScriptApp.getProjectKey())
.setPropertyStore(PropertiesService.getUserProperties())
.setScope(scope)
.setCallbackFunction('authCallback');
}
function authCallback(request) {
var driveService = getDriveService();
var isAuthorized = driveService.handleCallback(request);
if (isAuthorized) {
return HtmlService.createHtmlOutput('Success! You can close this tab.');
} else {
return HtmlService.createHtmlOutput('Denied. You can close this tab');
}
}
But this gives me an error "Access not granted or expired" when it tries to run the line:
var accessToken = googleOAuth_('google', scope).getAccessToken();
So I found a apps ScriptApp Method getOAuthToken which seemed like it might give me the token I need. I replaced the above line with:
var accessToken = ScriptApp.getOAuthToken();
And the code executes but my response from the server is "Error 403 User credentials required".
Here's my third attempt based on #Mogsdad's suggestion:
function sendPrintJob(docId,myPrinterId,docTitle) {
var payloadOfSubmit = {
"printerid" : myPrinterId,
"title" : docTitle,
"content" : docId,
"contentType" : "google.kix" ,
};
var request = {
"method": "POST",
"headers":{"Authorization": "Bearer "+ScriptApp.getOAuthToken()},
"muteHttpExceptions": true
};
var responseOfSubmit = UrlFetchApp.fetch("https://www.google.com/cloudprint/submit", request);
Logger.log(responseOfSubmit);
}
I've tried a number of variations, including creating a Developer Console Project and using the Client ID provided there, but I keep getting stuck at these two issues (access not granted, or credentials required). If anyone can provide any help I'd really appreciate it.
Here are the steps that allowed me to connect Google Apps Script to Google Cloud Print, so I could then submit GCP jobs (these steps are all started from within Google Apps Script):
Add the OAuth2 library
(https://github.com/googlesamples/apps-script-oauth2) to your Google
Apps Script by going to: Resources > Libraries > Find Library
MswhXl8fVhTFUH_Q3UOJbXvxhMjh3Sh48 > Select
Create new web application in Developer Console Resources > Developer Console Project > Click the project link > APIs & Auth >
Credentials > Add Credentials > OAuth2.0 Client ID > Web
Application > Set Authorized redirect URIs to the format
https://script.google.com/macros/d/{PROJECT KEY}/usercallback
where project key is under File > Project Properties and copy
your client ID and client secret
Add the ID and Secret to "getCloudPrintService()" code below (replace client_id and client_secret)
Go to Run > ShowURL and authorize the script.
Open the Logger (Cmd + Enter), copy the URL and paste it in a new browser tab to complete the authorization.
Go to https://www.google.com/cloudprint/#printers , select your printer, click details, expand advanced details, and copy your Printer ID (it will be of the format 555aa555-5a55-5555-5555-55555a55a555)
Add the printer id to "printGoogleDocument()" code below (replace myPrinterId)
This resource was helpful in figuring the steps out: http://ctrlq.org/code/20061-google-cloud-print-with-apps-script, and you may also find these links helpful:
https://developers.google.com/cloud-print/docs/appInterfaces
https://mashe.hawksey.info/2015/10/setting-up-oauth2-access-with-google-apps-script-blogger-api-example/
function showURL() {
var cpService = getCloudPrintService();
if (!cpService.hasAccess()) {
Logger.log(cpService.getAuthorizationUrl());
} else {
Logger.log("You already have access to this service.");
}
}
function getCloudPrintService() {
return OAuth2.createService('print')
.setAuthorizationBaseUrl('https://accounts.google.com/o/oauth2/auth')
.setTokenUrl('https://accounts.google.com/o/oauth2/token')
.setClientId(client_id)
.setClientSecret(client_secret)
.setCallbackFunction('authCallback')
.setPropertyStore(PropertiesService.getUserProperties())
.setScope('https://www.googleapis.com/auth/cloudprint')
.setParam('login_hint', Session.getActiveUser().getEmail())
.setParam('access_type', 'offline')
.setParam('approval_prompt', 'force');
}
function authCallback(request) {
var isAuthorized = getCloudPrintService().handleCallback(request);
if (isAuthorized) {
return HtmlService.createHtmlOutput('You can now use Google Cloud Print from Apps Script.');
} else {
return HtmlService.createHtmlOutput('Cloud Print Error: Access Denied');
}
}
function getPrinterList() {
var response = UrlFetchApp.fetch('https://www.google.com/cloudprint/search', {
headers: {
Authorization: 'Bearer ' + getCloudPrintService().getAccessToken()
},
muteHttpExceptions: true
}).getContentText();
var printers = JSON.parse(response).printers;
for (var p in printers) {
Logger.log("%s %s %s", printers[p].id, printers[p].name, printers[p].description);
}
}
function printGoogleDocument(docId, docTitle) {
// For notes on ticket options see https://developers.google.com/cloud-print/docs/cdd?hl=en
var ticket = {
version: "1.0",
print: {
color: {
type: "STANDARD_COLOR"
},
duplex: {
type: "NO_DUPLEX"
},
}
};
var payload = {
"printerid" : myPrinterId,
"content" : docId,
"title" : docTitle,
"contentType" : "google.kix", // allows you to print google docs
"ticket" : JSON.stringify(ticket),
};
var response = UrlFetchApp.fetch('https://www.google.com/cloudprint/submit', {
method: "POST",
payload: payload,
headers: {
Authorization: 'Bearer ' + getCloudPrintService().getAccessToken()
},
"muteHttpExceptions": true
});
// If successful, should show a job here: https://www.google.com/cloudprint/#jobs
response = JSON.parse(response);
if (response.success) {
Logger.log("%s", response.message);
} else {
Logger.log("Error Code: %s %s", response.errorCode, response.message);
}
return response;
}
The scope "https://www.googleapis.com/auth/cloudprint" has to be included explicitly in the
manifest file
appscript.json (View > Show manifest file)
{
"timeZone": "Europe/Paris",
"dependencies": {
},
"oauthScopes": [
"https://www.googleapis.com/auth/documents",
"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/spreadsheets",
"https://www.googleapis.com/auth/cloudprint"
],
"exceptionLogging": "STACKDRIVER"
}
Code.gs
function listPrinters() {
var options = {
headers: {
authorization: 'OAuth ' + ScriptApp.getOAuthToken()
}
}
var response = UrlFetchApp.fetch('https://www.google.com/cloudprint/search', options);
Logger.log(response);
}

OAuth in Google Apps Scripts Libraries

I am building some wrapper libraries in GAS for some of the Domain Admin APIs. I have a general library which handles OAuth, UrlFetch, XML to Object and Object to XML functions. I have added this as a Library to my API wrappers. The first one I created was for the EmailAuditAPI. This works just fine when I load the EmailAuditAPI as a library. The second API wrapper I created was for the GmailSettingsAPI. This wrapper works fine within itself, but is not working when I call it from another script where it has been loaded as a library. For the more visual of you
GoogleAPITools -> EmailAuditAPI Wrapper -> Project Script : Works!
GoogleAPITools -> GmailSettingsAPI Wrapper -> Project Script : Doesn't work!
GoogleAPITools -> GmailSettingsAPI Wrapper : Works!
I think this is the important code:
GoogleAPITools
function callGApi(apiUrl,authScope,method,payloadIn)
{
//oAuth & Fetch Arguments
var fetchArgs = googleOAuth_("google", authScope);
fetchArgs.method = method ? method : "GET";
if(payloadIn)
{
fetchArgs.contentType = "application/atom+xml";
fetchArgs.payload = payloadIn;
}
var urlFetch = UrlFetchApp.fetch(apiUrl, fetchArgs); //This line fails w/ not working wrapper
var returnObject = urlFetch.getContentText() != '' ? Xml.parse(urlFetch.getContentText()) : urlFetch.getResponseCode();
return returnObject;
}
//Google oAuth
function googleOAuth_(name,scope)
{
var oAuthConfig = UrlFetchApp.addOAuthService(name);
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("anonymous");
oAuthConfig.setConsumerSecret("anonymous");
return {oAuthServiceName:name, oAuthUseToken:"always"};
}
This wrapper works when loaded into a third script as a library.
function getAccountInfoRequest(user,reqId)
{
var properties = GoogleAPITools.callGApi(ACCOUNTINFOURL + user + "/" + reqId,AUDITSCOPE).entry.property;
return GoolgeAPITools.returnXmlToObject(properties);
}
This wrapper does not work when loaded into a third script as a library, but it does work on its own. The GoogleAPITools is loaded as a library in both wrapper scripts.
GmailSettingsAPI Wrapper
//Get Signature
function getSignatureForUser(user)
{
var returnedInfo = GoogleAPITools.callGApi(EMAILSETTINGSURL + user + "/signature",EMAILSETTINGSSCOPE);
return GoolgeAPITools.returnXmlToObject(new Array(returnedInfo.entry.property));
}
Third project script with both wrappers loaded as libraries:
function testApiWrappers() {
var user = "john";
var reqId = "123456789";
Logger.log(AuditApi.getAccountInfoRequest(user,reqId));
Logger.log(GmailSettingsApi.getSignatureForUser(user)); //Fails here
}
In my log I see the return for the AuditApi call, but I get an "Unexpected error:" that references the URLFetch in the GoogleAPITools script.
Is there something different about the Oauth for the two APIs? Is there something glaring in my code that I missed? Any assistance would be great.