I wish to authorize my webapp to create a folder in the user's appfolder to hold the app's data files.
To do this, I need to request the scope https://www.googleapis.com/auth/drive.appfolder
So far I have the following code:
var CLIENT_ID = '3941...';
var CLIENT_SECRET = 'DY_P...';
var SCRIPT_ID = '1XAF...';
var appfolder_scope = 'https%3A%2F%2Fwww.googleapis.com%2Fauth%2Fdrive.appfolder';
var redirectURI = 'https%3A%2F%2Fscript.google.com%2Fmacros%2Fd%2F'+ SCRIPT_ID + '%2Fusercallback';
var AuthEndpoint = 'https://accounts.google.com/o/oauth2/v2/auth';
function getCallbackURL(callbackFunction) {
var url = ScriptApp.getService().getUrl(); // Ends in /exec (for a web app)
url = url.slice(0, -4) + 'usercallback?state='; // Change /exec to /usercallback
var stateToken = ScriptApp.newStateToken()
.withMethod(callbackFunction)
.withTimeout(120)
.createToken();
return url + stateToken;
}
function generateAuthRequestURL() {
var AuthRequest = AuthEndpoint;
var Query = '?'
+ 'scope=profile%20' + appfolder_scope
+ '&state=' + getCallbackURL(cb)
+ '&redirect_uri=' + redirectURI
+ '&response_type=code'
+ '&client_id=' + CLIENT_ID
//+ '&login_hint=...%40gmail.com'
;
AuthRequest += Query;
Logger.log(AuthRequest);
return AuthRequest;
}
function cb(response) {
Logger.log(response);
}
When I click on the url generated by the generateAuthRequestURL() it takes me to the consent screen where I click allow. But then every time I get 'The state token is invalid or has expired'.
The webapp is published and I have tested both the exec and dev versions with the same result. I have also tried with and without a login_hint.
I have also experimented with Apps-Script-Folder-Library as well as gdrive-appdata. I couldn't get the first one to work, and the second one I don't even know how to use.
Based from this thread, make sure that you've put the project key into the related field. This error may occur when the key changed after making a copy of the script.
You can also check on this issue which suggested to ensure that you are using the right project key.
Here are some related forums which might also help:
Issues with OAuth 2.0 Library for Google Apps Scripts
OAuth2 support through library returns The state token is invalid or has expired. Please try again
Related
I am attempting to build a web app that will create a Google Doc from a template and populate it with user provided data. Using Google's quickstart example in the documentation, I can successfully authorize and access the Google Drive file system. Now I need to programmatically open a template Google Doc (or even create one from scratch) and add the data.
This is rather easily done using App Script's Document Service (the DocumentAppclass). So I can do something like:
function createDoc(contentArray) {
var doc = DocumentApp.create('Sample Document');
var body = doc.getBody();
var rowsData = contentArray; // data submitted with HTML form passed as arg
body.insertParagraph(0, doc.getName())
.setHeading(DocumentApp.ParagraphHeading.HEADING1);
table = body.appendTable(rowsData);
table.getRow(0).editAsText().setBold(true);
}
in a standalone App Script and successfully create the new Google Doc on Google Drive. I can't figure out how to execute this App Script from my external web app. Is there a way to do this or do I need to find a different way to create Google Docs (and add content) using just the Drive API?
EDIT:
here is the GET request from my web app:
var gurl = "https://script.google.com/macros/s/AKfycbwMHKzfZr1X06zP2iEB4E8Vh-U1vGahaLjXZA1tk49tBNf0xk4/exec";
$.get(
gurl,
{ name: "john", time: "2pm",},
function(data) {
console.log(data);
},
"jsonp"
)
and here is my doGet():
function doGet(e) {
var result = "";
var name = e.parameter.name;
Logger.log(name);
try {
result = "Hello " + name;
} catch (f) {
result = "Error: " + f.toString();
}
result = JSON.stringify({
"result": result
});
var doc = DocumentApp.create('ballz3');
var body = doc.getBody();
var rowsData = [['Plants', 'Animals'], ['Ficus', 'Goat'], ['Basil', 'Cat'], ['Moss', 'Frog']];
body.insertParagraph(0, doc.getName())
.setHeading(DocumentApp.ParagraphHeading.HEADING1);
table = body.appendTable(rowsData);
table.getRow(0).editAsText().setBold(true);
Logger.log('DOc Name: ' + doc.getName());
return ContentService
.createTextOutput(e.parameter.callback + "(" + result + ")")
.setMimeType(ContentService.MimeType.JAVASCRIPT);
}
In order to run a script from an external location like your web app you need to follow some set-up and use the Script API. The documentation provided below has a great example on how to run your scripts from outside.
Additionally, you can use the APIs directly, with services like OAuth to use the APIs directly in a way that can save you some time and make your code more simple. Using OAuth can provide you with simple API requests. To use it:
Go to the link provided below, select the desired scope (Drive for this example).
Exchange the authorization token for refresh/access tokens.
Proceed to configure the request. Here you can set all the parameters for the request, and even select from the existing operations the scope has available (“List possible operations” button).
The resulting request will look like the one below:
GET /drive/v3/files HTTP/1.1 Host: www.googleapis.com Content-length:
0 Authorization: Bearer [YOUR-TOKEN]
Beneath it you will see the server response to the request.
Documentation URL: https://developers.google.com/apps-script/api/how-tos/execute
OAuth Playground: https://developers.google.com/oauthplayground/
This is a follow-up to a previous question/answer I posted (How to use the Google Email Settings API and the OAuth2 for Apps Script Library to set email signatures for users in a Google Apps domain), but I'm creating a new question since the Email Settings API has been deprecated and the process is significantly different now.
As the administrator of a G Suite domain, how do you use the Gmail API to programmatically set the email signatures of users in your domain through Google Apps Script?
This method uses the Gmail API, the OAuth2 for Apps Script library, and "Domain-wide Delegation of Authority", which is a way for G Suite admins to make API calls on behalf of users within their domain.
Step 1: Make sure the OAuth2 For Apps Script library is added to your project.
Step 2: Set up "Domain-Wide Delegation of Authority." There's a page here explaining how to do it for the Drive API, but it's pretty much the same for any Google API, including the Gmail API. Follow the steps on that page up to, and including, the "Delegate domain-wide authority to your service account" step.
Step 3: The code below includes how to set the signature after the previous steps are complete:
function setSignatureTest() {
var email = 'test#test.com';
var signature = 'test signature';
var test = setSignature(email, signature);
Logger.log('test result: ' + test);
}
function setSignature(email, signature) {
Logger.log('starting setSignature');
var signatureSetSuccessfully = false;
var service = getDomainWideDelegationService('Gmail: ', 'https://www.googleapis.com/auth/gmail.settings.basic', email);
if (!service.hasAccess()) {
Logger.log('failed to authenticate as user ' + email);
Logger.log(service.getLastError());
signatureSetSuccessfully = service.getLastError();
return signatureSetSuccessfully;
} else Logger.log('successfully authenticated as user ' + email);
var username = email.split("#")[0];
var resource = { signature: signature };
var requestBody = {};
requestBody.headers = {'Authorization': 'Bearer ' + service.getAccessToken()};
requestBody.contentType = "application/json";
requestBody.method = "PUT";
requestBody.payload = JSON.stringify(resource);
requestBody.muteHttpExceptions = false;
var emailForUrl = encodeURIComponent(email);
var url = 'https://www.googleapis.com/gmail/v1/users/me/settings/sendAs/' + emailForUrl;
var maxSetSignatureAttempts = 20;
var currentSetSignatureAttempts = 0;
do {
try {
currentSetSignatureAttempts++;
Logger.log('currentSetSignatureAttempts: ' + currentSetSignatureAttempts);
var setSignatureResponse = UrlFetchApp.fetch(url, requestBody);
Logger.log('setSignatureResponse on successful attempt:' + setSignatureResponse);
signatureSetSuccessfully = true;
break;
} catch(e) {
Logger.log('set signature failed attempt, waiting 3 seconds and re-trying');
Utilities.sleep(3000);
}
if (currentSetSignatureAttempts >= maxSetSignatureAttempts) {
Logger.log('exceeded ' + maxSetSignatureAttempts + ' set signature attempts, deleting user and ending script');
throw new Error('Something went wrong when setting their email signature.');
}
} while (!signatureSetSuccessfully);
return signatureSetSuccessfully;
}
// these two things are included in the .JSON file that you download when creating the service account and service account key
var OAUTH2_SERVICE_ACCOUNT_PRIVATE_KEY = '-----BEGIN PRIVATE KEY-----\nxxxxxxxxxxxxxxxxxxxxx\n-----END PRIVATE KEY-----\n';
var OAUTH2_SERVICE_ACCOUNT_CLIENT_EMAIL = 'xxxxxxxxxxxxxxxxxxxxx.iam.gserviceaccount.com';
function getDomainWideDelegationService(serviceName, scope, email) {
Logger.log('starting getDomainWideDelegationService for email: ' + email);
return OAuth2.createService(serviceName + email)
// Set the endpoint URL.
.setTokenUrl('https://accounts.google.com/o/oauth2/token')
// Set the private key and issuer.
.setPrivateKey(OAUTH2_SERVICE_ACCOUNT_PRIVATE_KEY)
.setIssuer(OAUTH2_SERVICE_ACCOUNT_CLIENT_EMAIL)
// Set the name of the user to impersonate. This will only work for
// Google Apps for Work/EDU accounts whose admin has setup domain-wide
// delegation:
// https://developers.google.com/identity/protocols/OAuth2ServiceAccount#delegatingauthority
.setSubject(email)
// Set the property store where authorized tokens should be persisted.
.setPropertyStore(PropertiesService.getScriptProperties())
// Set the scope. This must match one of the scopes configured during the
// setup of domain-wide delegation.
.setScope(scope);
}
Please note: the do-while loop with the maxSetSignatureAttempts and currentSetSignatureAttempts variables is not necessary. I added it because if you're trying to set signatures immediately after creating the Google account and assigning a G Suite license, sometimes the Gmail API returns an error as if the user wasn't created yet. That do-while loop basically waits 3 seconds if it gets an error, then tries again, up to x number of times. You shouldn't have that issue if you're setting signatures for existing users. Also, originally I just had a fixed 10-second sleep, but most of the time it didn't need to take that long, but other times it would still fail. So this loop is better than a fixed sleep amount.
I'm trying to use the CloudKit Web Services API to fetch Article records from my production CloudKit container's public database within Google Apps Script.
My request is based on the documentation on this page.
Here's my code:
// variables for the CloudKit request URL
var path = "https://api.apple-cloudkit.com";
var version = "1";
var container = "iCloud.com.companyname.My-Container-Name";
var environment = "production";
var database = "public";
var token = "8888888888888_my_actual_token_88888888888888888"
function showArticles() {
// assemble the URL
var url = path + "/database/" + version + "/" + container + "/" + environment + "/" + database + "/records/query?ckAPIToken=" + token;
// specify the record type to query
var query = {
recordType: "Article"
};
// specify the payload for the POST request
var payload = {
query : query
};
// set up the fetch options for the fetch request
var options = {
method : "POST",
payload : payload
};
// make the request
var response = UrlFetchApp.fetch(url, options);
Logger.log(response);
}
UrlFetchApp.fetch(url, options) fails with this error:
Request failed for https://api.apple-cloudkit.com/database/1/iCloud.<?>-Container-Name/development/public/records/query?ckAPIToken=8888888888888_my_actual_token_88888888888888888 returned code 401. Truncated server response: {"uuid":"7d8a8547-ad08-4090-b4b3-917868a42f6f","serverErrorCode":"AUTHENTICATION_FAILED","reason":"no auth method found"} (use muteHttpExceptions option to examine full response) (line 30, file "Code")
I've been troubleshooting for a few hours and I can't figure out what I'm doing wrong. I've tried it with a separate token on my development environment, too, and the same thing happens.
This page mentions the ckWebAuthToken parameter and says "if omitted and required, the request fails," but I can't find anything that says what requests require a ckWebAuthToken. I'm assuming I don't need ckWebAuthToken since the records I'm trying to access are in my container's public database, and I'm getting an AUTHENTICATION_FAILED error rather an AUTHENTICATION_REQUIRED error.
One part that confuses me is this URL that comes up in the error message:
https://api.apple-cloudkit.com/database/1/iCloud.<?>-Container-Name/development/public/records/query?ckAPIToken=8888888888888_my_actual_token_88888888888888888
I would expect it to be:
https://api.apple-cloudkit.com/database/1/iCloud.com.companyname.My-Container-Name/development/public/records/query?ckAPIToken=8888888888888_my_actual_token_88888888888888888
But I can't tell if that's actually the URL that's being requested, and when I log the url variable everything looks fine.
Thanks in advance for any troubleshooting tips or solutions!
UPDATE
I tried using Postman, and the request worked with same endpoint and POST data. It looks like the container component of the URL is getting corrupted by the Google Apps Script UrlFetchApp.fetch() method. The <?> seems to only show up when com. is in the URL.
I'm not sure why this is the answer, but I was able to get it working by using JSON.stringify() on the payload in options:
var options = {
method : "POST",
payload : JSON.stringify(payload)
};
I am looking for a method to find the unread message count of delegated mailboxes from any of the Google API's.
I am not sure IF it is possible, but it would help me develop a helping tool for a company using this for 1000+ users. A lot of delegation is going on, and I am eager to find a way to accomplish this.
But I might need some help, maybe from people closer with knowledge of the possibilities of the Admin SDK from Google.
I want to use Google Apps Script to collect the unread message count.
The Email Settings API allows you to see which delegations are in place.
It is not possible for a user to access the mailbox of another user who has delegated them access via IMAP, thus you can't authenticate as a user and check the delegated mailbox.
You should use OAuth 2.0 Service Accounts to authenticate to the mailboxes via IMAP.
Once authenticated you can select the Gmail "All Mail" folder (or Inbox if you only want count for Inbox). and do a Gmail search of "is:unread" to determine how many unread messages the user has.
FYI, my open-source app, GYB can do just this. There is a getting started guide for GYB. You'll also need to setup the service account. The command to get unread message count for all mail would be something like:
gyb --email delegated-mailbox#yourcompany.com --service-account your-service#account.com --action count --search "is:unread"
I got my answer from the GAS community on Google Plus, so credits to the posters there.
https://plus.google.com/106333172328928589411/posts/7g3Vu7iFZfb
Sergii:
Check out this gist that shows how to do 2-legged OAuth authentication in GAS https://gist.github.com/rcknr/c5be4eb80d821158c8ef.
Using 2 Legged Oauth you can get access to the ATOM feed of other users:
A piece of working code for it:
function gmail2lo(user) {
var OAUTH_CONSUMER_SECRET = 'secret';
var domain = 'domain'; //use the domain as key in apps panel
var username = 'user';
var xuser = username+'#'+domain;
var method = "GET";
var baseUrl = "https://mail.google.com/mail/feed/atom";
var timestamp = Math.round(new Date().getTime() / 1000);
var paramsJson;
var paramsOauth = {
oauth_consumer_key : domain,
oauth_nonce : timestamp,
oauth_signature_method : "HMAC-SHA1",
oauth_timestamp : timestamp,
oauth_version : "1.0",
'xoauth_requestor_id' : xuser
};
var paramsStringArray = [];
for (var k in paramsJson)
paramsStringArray.push(k + '=' + encodeURIComponent(paramsJson[k]));
var oauthStringArray = [];
for (var k in paramsOauth)
oauthStringArray.push(k + '=' + encodeURIComponent(paramsOauth[k]));
var paramsString = paramsStringArray.concat(oauthStringArray).sort().join('&');
var signatureBaseString = method +"&"+ encodeURIComponent(baseUrl) +"&"+ encodeURIComponent(paramsString);
var signatureBytes = Utilities.computeHmacSignature(Utilities.MacAlgorithm.HMAC_SHA_1, signatureBaseString, OAUTH_CONSUMER_SECRET+'&');
var signature = encodeURIComponent(Utilities.base64Encode(signatureBytes));
var xoauthString = 'OAuth ' + oauthStringArray.sort().slice(0,oauthStringArray.length-1).join(", ") + ', oauth_signature=' + signature;
var ooptions = {
headers : {authorization: xoauthString}
}
url = baseUrl;
url += "?" + paramsStringArray.join("&") + '&xoauth_requestor_id=' + encodeURIComponent(xuser);
var response = UrlFetchApp.fetch(url, ooptions).getContentText();
}
As a test case, I'm trying to copy a file from Google Drive to Dropbox using Google Scripts
function pushBuild() {
// Setup OAuthServiceConfig
var oAuthConfig = UrlFetchApp.addOAuthService("dropbox");
oAuthConfig.setAccessTokenUrl("https://api.dropbox.com/1/oauth/access_token");
oAuthConfig.setRequestTokenUrl("https://api.dropbox.com/1/oauth/request_token");
oAuthConfig.setAuthorizationUrl("https://www.dropbox.com/1/oauth/authorize");
oAuthConfig.setConsumerKey(ScriptProperties.getProperty("dropboxKey"));
oAuthConfig.setConsumerSecret(ScriptProperties.getProperty("dropboxSecret"));
var fileName = "blah.zip"
var folderName = "upload_dir"
var docs = DocsList.getFolder(folderName).find(fileName);
for(n=0;n<docs.length;++n){
if(docs[n].getName() == fileName){
var ID = docs[n].getId();
var options = {
"oAuthServiceName" : "dropbox",
"oAuthUseToken" : "always",
"method" : "put",
"payload" : docs[n].getBlob().getBytes(),
"contentType" : "application/zip"
};
var response = UrlFetchApp.fetch("https://api-content.dropbox.com/1/files_put/sandbox/upload_dir/" + fileName, options);
Logger.log(response);
}
}
}
The authorization request for the application in Dropbox appears and it tells me that I've successfully authorized my app, but when I check, the app is not in the list of "My Apps", the file has not been uploaded and there are no entries in the log. The directory "upload_dir" exists on both GD and DB. I've tried the same code with "App Folder" and "Full Dropbox" app types, but get the same result.
Additionally, running the script again once again triggers the Authorization page, similar to
to appear, clicking "Allow" then shows the success screen but the application is not listed in "My Apps". Running the script again repeats the process.
Can anyone point out what I've done wrong?
Update
So, I've now tried to implement this using the individual api calls and am still not having any success.
function testOAuth() {
var timestamp = getTimestamp();
var nonce = getNonce(timestamp);
var authString = 'OAuth oauth_version="1.0", oauth_signature_method="PLAINTEXT", oauth_signature="' + encodeURIComponent(ScriptProperties.getProperty("dropboxSecret") + '&') + '", oauth_consumer_key="' + ScriptProperties.getProperty("dropboxKey") + '"';
Logger.log(authString)
var options = {
method : "POST",
headers : {"Authorization" : authString}
}
var response = UrlFetchApp.fetch("https://api.dropbox.com/1/oauth/request_token",options);
var params = response.getContentText().split("&");
var map = new Map;
for(i = 0; i < params.length; i++){
var param = params[i].split("=");
map.put(param[0],param[1]);
}
var authStringx = "https://www.dropbox.com/1/oauth/authorize?oauth_token=" + map.get("oauth_token");
Logger.log(authStringx);
var response2 = UrlFetchApp.fetch(authStringx);
Logger.log(response2.getContentText());
var authString2 = 'OAuth oauth_version="1.0", oauth_signature_method="PLAINTEXT", oauth_token="' + map.get("oauth_token") + '" , oauth_signature="' + encodeURIComponent(ScriptProperties.getProperty("dropboxSecret") + '&' + map.get("oauth_token_secret")) + '", oauth_consumer_key="' + ScriptProperties.getProperty("dropboxKey") + '",oauth_timestamp="'+ timestamp +'", oauth_nonce="' + nonce +'"';
Logger.log(authString2);
var options3 = {
"method" : "POST",
"Authorization" : authString2
}
var response3 = UrlFetchApp.fetch("https://api.dropbox.com/1/oauth/access_token", options3);
Logger.log(response3.getContentText());
}
var getTimestamp = function(){
return (Math.floor((new Date()).getTime() / 1000)).toString()
}
var getNonce = function(timestamp){
return timestamp + Math.floor( Math.random() * 100000000)
}
The code implementation for the map is here. The main problem that I can see is that authorize step does not invoke the Dropbox authorize end point (ie no browser redirection takes place to authorize the application). If I place a breakpoint just after the line Logger.log(authStringx); and manually visit the web page pasting in the contents of authStringx I get the screen to authorize my app. I accept that and get the message that the app is registered in "My Apps". I now let the program continue and I am greeted with the message
Any ideas?
Pram,
I was trying to accomplish the same task and came across your post. I am not a programmer, so I can't figure out the second part either (launching the authorization page fails), but I was able to complete the process in the third step and connect my app successfully.
Instead of:
var authString2 = 'OAuth oauth_version="1.0", oauth_signature_method="PLAINTEXT", oauth_token="' + map.get("oauth_token") + '" , oauth_signature="' + encodeURIComponent(ScriptProperties.getProperty("dropboxSecret") + '&' + map.get("oauth_token_secret")) + '", oauth_consumer_key="' + ScriptProperties.getProperty("dropboxKey") + '",oauth_timestamp="'+ timestamp +'", oauth_nonce="' + nonce +'"';
Logger.log(authString2);
var options3 = {
"method" : "POST",
"Authorization" : authString2
}
var response3 = UrlFetchApp.fetch("https://api.dropbox.com/1/oauth/access_token", options3);
Logger.log(response3.getContentText());
I used:
var authtokenURL = "https://api.dropbox.com/1/oauth/access_token";
var authString2 = "?oauth_signature_method=PLAINTEXT&oauth_token=" + [MY_OAUTH_REQUEST_TOKEN] + "&oauth_signature=" + encodeURIComponent([MY_DROPBOX_CONSUMER_SECRET] + "&" + [MY_OAUTH_REQUEST_SECRET]) +"&oauth_consumer_key=" + [MY_DROPBOX_CONSUMER_KEY];
Logger.log(authString2);
var response3 = UrlFetchApp.fetch("https://api.dropbox.com/1/oauth/access_token" + authString2);
Logger.log(response3.getContentText());
I then got an email confirmation that I connected a new app to Dropbox, and my app does show up under Settings in my account. Anyway, as I said, I'm no programmer, so sorry for the ugly code. Thanks for supplying your code for me to make it this far. I hope this helps you at least move forward, even if it doesn't solve the underlying problem.
I am able to see this issue as well. There is something special going on here with Dropbox. You should check on their forums or with their API support team. Looks like they are not correctly accepting callback params. Perhaps this is a development mode limitation (vs. production mode). Or perhaps they are stringent about some POST vs GET differences that Google doesn't support.
This code below exhibits the same issue you described where the authorization is never complete.
function dropbox() {
var oAuthCfg = UrlFetchApp.addOAuthService("dropbox");
oAuthCfg.setAccessTokenUrl('https://api.dropbox.com/1/oauth/access_token');
oAuthCfg.setRequestTokenUrl('https://api.dropbox.com/1/oauth/request_token');
oAuthCfg.setAuthorizationUrl('https://api.dropbox.com/1/oauth/authorize');
oAuthCfg.setConsumerKey('DROPBOX_KEY');
oAuthCfg.setConsumerSecret('DROPBOX_SECRET');
var options = {oAuthServiceName:'dropbox',oAuthUseToken:'always'}
var url = 'https://api.dropbox.com/1/account/info';
var response = UrlFetchApp.fetch(url, options).getContentText();
Logger.log(response);
}
However, the same code works without issue with the Twitter OAuth 1 API. The code below should dump out JSON from your stream (once you substitute the tokens from your setup in http://dev.twitter.com
function twitter(){
var oAuthCfg = UrlFetchApp.addOAuthService('twitter');
oAuthCfg.setAccessTokenUrl('http://api.twitter.com/oauth/access_token');
oAuthCfg.setRequestTokenUrl('http://api.twitter.com/oauth/request_token');
oAuthCfg.setAuthorizationUrl('http://api.twitter.com/oauth/authorize');
oAuthCfg.setConsumerKey('TWITTER_KEY');
oAuthCfg.setConsumerSecret('TWITTER_SECRET');
var options = {oAuthServiceName:'twitter',oAuthUseToken:'always'}
var url = "http://api.twitter.com/1/statuses/user_timeline.json";
var response = UrlFetchApp.fetch(url, options).getContentText();
Logger.log(response);
}
If you are able to narrow this down to a Google issue log a bug here on the Issue Tracker.