I'm trying to connect to the Facebook Graph API via a Google Apps Script but I'm getting an error
I've tried:
function myFunction (name) {
FB.init({
appId : '{your-app-id}',
status : true,
xfbml : true,
version : 'v2.0'
});
var jsonData = UrlFetchApp.fetch("graph.facebook.com/"; + name);
}
I've also tried:
function myFuntion(name) {
window.fbAsyncInit = function() {
FB.init({
appId : 'your-app-id',
xfbml : true,
version : 'v2.0'
});
};
var jsonData = UrlFetchApp.fetch("graph.facebook.com/"; + name);
}
but neither have worked, I always get a:
"ReferenceError: "FB" is not defined." and a "ReferenceError: "window" is not
defined"
and
"(#4) Application request limit reached","type":"OAuthException","code":4}}
despite putting in my facebook app ID into the variable. I know that "window" is part of an external javascript library so that's why I'm unable to use it in a Google Apps Script, but even after looking at other posts I'm still confused on why I get a "FB" is not defined error.
Any ideas on how to solve this?
There are error codes at the bottom of this page:
Facebook Graph API - Error codes
The "OAuthException" has to do with the Login Status. If you get that error, then you aren't logged in, and to get what you want, you need to be logged in.
You can get an App Access Token using a Server to Server request. There are four types of
Access Tokens:
User - to read, modify or write a specific person's Facebook data on their behalf.
App - modify and read the app settings, and publish Open Graph actions.
Page - read, write or modify the data belonging to a Facebook Page.
Client - the client token is used rarely. Very limited Access to Facebook.
Forms of Access Tokens
User access tokens come in two forms: short-lived tokens and long-lived tokens
short-lived - lifetime of about an hour or two - generated via web login
long-lived - lifetime of about 60 days
You probably don't have an App Access Token. You have an App ID, but that's different than an App Token.
You only get your App Token once. You need to run some code to get it.
Note, that you also must know your App Secret in order to run this code. If you don't know, or have your App Secret, then you need to get that.
See if you can run this code:
//A Facebook App Token never changes unless you go to the Facebook Developers Console,
//and you
//change the App Secret. So, do NOT keep requesting a new App Token. Just get it once,
//then
//hard code it into a backend secret function.
// The App Token can be used to modify your App, but you can just do that 'Manually'
function getOneTimeFB_AppToken() {
Logger.log("getOneTimeFB_AppToken ran");
//keep this info secret
//Generate an App Access Token
var myApp_ID = 'Your App ID';
var myAppSecret = 'Your App Secret';
var optnAppTkn = {"method" : "get"};
var getAppTknURL = "https://graph.facebook.com/oauth/access_token?client_id=" + myApp_ID + "&client_secret=" + myAppSecret + "&grant_type=client_credentials"
var getAppTkn = UrlFetchApp.fetch(getAppTknURL, optnAppTkn);
Logger.log("Object returned from GET: " + getAppTkn)
var myAppTkn = getAppTkn.getContentText();
Logger.log("myAppTkn: " + myAppTkn);
};
Run that code, then in the script editor, choose the VIEW menu, and the LOGS menu item. Read what is in the LOGS. Don't keep running this code over and over again. Just run it once if it's successful.
If that code works, then you just successfully communicated with Facebook.
You need to understand what the Tokens can do, and what your options are. If you are not going to get a token from a user through client side authorization, then you need to understand the App Token.
App Tokens allow you to interact with Facebook on behalf of an app rather than a user. This can be used to read YOUR app insights and modify the parameters of YOUR app.
You never want to use an App Token in client side (browser) code. That would be a major security problem.
However, if a user has granted your application publishing permissions, then you can use the App Token to publish content to Facebook on behalf of that person. So, app access token can be used in place of a user access token to make API calls IF your app has been granted publishing permissions.
But how do you get publishing permissions? Well, there is no way to get the initial short term access token through the server. That just makes sense if you think about it in terms of security. You can't get the initial, short term access token any other way than through a client login. So, if you want to do something that isn't within the bounds of the App Access Token, you can't do it without having the user login through client side.
You can achieve a client side login, without using the JavaScript SDK. So, in the case of an Apps Script Stand Alone HTML web app, you can still use Facebook login without needing to link to the Facebook JavaScript SDK. If you need to do that, let me know.
In that code, FB is an object. The object needs to be assigned "key/value" pairs. Every "key/value" pair is an element (property) in the object. The error is directly related to how objects work. That FB object gets assigned values from a link (inside HTML) to the Facebook API. If you are trying to use an HTML link to the Facebook API from server side (.gs) code, it won't work. There are lots of things that could be going wrong. In order to know exactly what is going wrong, we need to know whether that code is in a gs file, or an HTML file inside a <script> tag.
There are a couple of ways to connect to Facebook:
From HTML (Client Side)
From the server with HTTP Requests
It looks like the code you are using is from an example of how to use the Facebook JavaScript SDK that is meant to run from inside HTML. The problem with that, is that Apps Script sanitizes HTML sent to the browser. So, if you try to link to the Facebook JavaScript SDK through the HTML, you may not get access. I know that, in the past, I have not been able to use a link to the Facebook API in HTML with the NATIVE sandboxed mode. I haven't tried the new IFRAME sandbox mode.
Related
I'm working on an performance evaluation app in Google App Maker. One of the challenges we have with our current tool is that it doesn't sync with our G Suite directory when a person's manager changes or when a person has a name change -- their existing evaluations are linked to the person's old name and we have to change manually.
In my new app, I have an Employees datasource that includes a relation to the evaluation itself that was initially populated via the Directory API. Reading the documentation here, it seems as though I should be able to set up a watch on the Users resource to look for user updates and parse through them to make the appropriate name and manager changes in my Employees datasource. What I can't figure out, though, is what the receiving URL should be for the watch request.
If anyone has done this successfully within Google App Maker, or even solely within a Google Apps Script, I'd love to know how you did it.
EDITED TO ADD:
I created a silly little GAS test function to see if I can get #dimu-designs solution below to work. Unfortunately, I just get a Bad Request error. Here's what I have:
function setUserWatch() {
var optionalArgs = {
"event": "update"
};
var resource = {
"id": "10ff4786-4363-4681-abc8-28166022425b",
"type": "web_hook",
"address": "https://script.google.com/a/.../...hXlw/exec"
};
AdminDirectory.Users.watch(resource);
}
Address is the current web app URL.
EDITED TO ADD MORE:
The (in)ability to use GAS to receive web hooks has been an active issue/feature request since Sep 2014 -- https://issuetracker.google.com/issues/36761910 -- which has been #dimu-designs on top of for some time.
This is a more comprehensive answer.
Google supports push notifications across many of their APIs. However there are many subtle (and not so subtle) differences between them. Some that leverage webhooks send their data payloads primarily as HTTP headers; for example Drive API and Calendar API. Others mix their payloads across HTTP headers and a POST body(ex: AdminDirectory API). And its gets even crazier, with some APIs utilizing different mechanisms altogether (ex: GMail API leverages Cloud PubSub).
There are nuances to each but your goal is to leverage AdminDirectory push notifications in a GAS app. To do that you need a GAS Web App whose URL can serve as a web-hook endpoint.
STEP 1 - Deploy A Stand-Alone Script As A Web App
Let's start with the following template script and deploy it as a Web App from the Apps Script Editor menu Publish > Deploy As Web App:
/** HTTP GET request handler */
function doGet(e) {
return ContentService.createTextOutput("GET message");
}
/** HTTP POST request handler */
function doPost(e) {
return ContentService.createTextOutput("POST message");
}
STEP 2 - Verify/Validate Domain Ownership And Add/Register Domain
NOTE: As of August 2019, GAS Web App URLs can no longer be verified using this method. Google Cloud Functions may be a viable
alternative.
With the web app deployed you now have to verify and register the domain of the receiving url, which in this case is also the web app url. This url takes the following form:
https://script.google.com/macros/s/xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx/exec
Technically you cannot own a GAS web app url's domain. Thankfully the App Script Gods at Google do provide a mechanism to verify and register a GAS web app url.
From the Apps Script Editor menu select Publish > Register in Chrome Web Store. Registering a published web app with the Chrome Web Store also validates the URL's domain (no need to fiddle with the search console).
Once validated you need to add the "domain" via the Domain verification page in the API Console. The "domain" is everything in the url sans the 'exec', so you'll add a string that looks like this:
https://script.google.com/macros/s/xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx/
STEP 3 - Make a watch request
For this step the AdminSDK/Directory API service should be enabled both for your App Script project and in the API Console.
Create a function that generates a watch request (this can be retooled for other event types):
function startUpdateWatch() {
var channel = AdminDirectory.newChannel(),
receivingURL = "https://script.google.com/macros/s/xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx/exec",
gSuiteDomain = "[business-name].com",
event = "update";
channel.id = Utilities.getUuid();
channel.type = "web_hook";
channel.address = receivingURL + "?domain=" + gSuiteDomain + "&event=" + event;
channel.expiration = Date.now() + 21600000; // max of 6 hours in the future; Note: watch must be renew before expiration to keep sending notifications
AdminDirectory.Users.watch(
channel,
{
"domain":gSuiteDomain,
"event":event
}
);
}
Note that Directory API push notifications have an expiration, the max being 6 hours from starting the watch so it must be renewed periodically to ensure notifications are sent to the endpoint URL. Typically you can use a time-based trigger to call this function every 5 hours or so.
STEP 4 - Update doPost(e) trigger to handle incoming notifications
Unlike the push mechanisms of other APIs, the Directory API sends a POST body along with its notifications, so the doPost(e) method is guaranteed to be triggered when a notification is sent. Tailor the doPost(e) trigger to handle incoming events and re-deploy the web app:
function doPost(e) {
switch(e.parameter.event) {
case "update":
// do update stuff
break;
case "add":
break;
case "delete":
break;
}
return ContentService.createTextOutput("POST message");
}
There is one caveat to keep in mind. Push notifications for update events only tell you that the user's data was updated, it won't tell you exactly what was changed. But that's a problem for another question.
Note that there are a ton of details I left out but this should be enough to get you up and running.
You can do this with GAS and the Admin SDK. The Directory API supports Notifications (Note this is scheduled to be deprecated so not sure what is replacing this functionality). You can then set up a GMAIL script to do what you need to do with the notification.
UPDATE: There are also PUSH notifications from the Directory API.
I was able to set up push notifications for a local resource (a spreadsheet) using Heroku-based Node.js app as an intermediary API. The Node app captures the custom request headers and builds the payload to be consumed by the doPost(e) function of the GAS web app.
The code for constructing a watch request is simple
//get the unique id
var channelId = Utilities.getUuid();
//build the resource object
var resource = {
"id": channelId,
"type": "web_hook",
"address": "https://yourapp.herokuapp.com/drivesub
}
//watch the resource
Drive.Files.watch(resource, fileId);
The challenge is to get that domain address verified. There are ways to verify the standalone (not file-bound!) GAS web app, however, as previous posters have mentioned, the Apps Script web app can't access custom headers.
After you've enabled the Pub/Sub API and created the topic & subscription, go to APIs & Services -> Credentials -> Domain verification. It gives you a few options of verifying your domain, including serving the html file. Download the file generated by Google. Thankfully, Heroku makes it very easy to deploy a Node app
https://devcenter.heroku.com/articles/getting-started-with-nodejs
After your domain is verified you can make your subscription push data to the endpoint URL on Heroku.
I simply created the js file for route handlers and created one specifically for domain verification
handlers.verifyDomain = function(callback){
//Synchronously read from the static html file. Async method fs.readFile() doesn't make the file available to the router callback
var file = fs.readFileSync('./google/google_file.html');
callback(200, file);
}
Then include the handler in your router object like so:
var router = {
"google_file.html": handlers.verifyDomain
}
Finally, in your server starting function, get the path from the URL (there are multiple ways of doing that), and execute the handler;
var routeHandler = router(path);
routerHandler(function(statusCode, file){
//do something
});
Go back to domain verification tool and load the HTML page to verify the URL. After it's verified, the only remaining step is creating the GAS web app and posting to it.
Back to Node app. Note that my endpoint is https://yourapp.herokuapp.com/drivesub
//The code for posting data to GAS web app. Of course, you need to
// update your router with router['driveSub'] = handlers.driveSub
handlers.driveSub = function(data, callback){
var headers = data.headers;
var options = {
method:"POST",
uri: your_gas_app_url,
json:{"headers":headers}
};
//Use NPM to install the request module
request.post(options, function(err, httpResponse, body){
console.log(err, body);
});
callback(200, data);
}
Apps Script app - don't forget to publish it.
function doPost(req) {
var postData = req.postData["contents"];
var headers = JSON.parse(postData)["headers"];
//take action
return 200;
}
Unfortunately you cannot, at least not solely using Apps Script.
Admin Directory push notifications require a web-hook URL endpoint to receive notifications. You might think deploying a GAS web app and using its URL as an endpoint would be sufficient. But the thing with Admin Directory Push notifications is that its data payload resides in custom HTTP headers which cannot be accessed from a GAS Web App. (This also holds true for push notifications across other APIs including the Drive and Calendar APIs)
You can however leverage Google Cloud Functions (a GCP service) in tandem with GAS, but you'll have to know your way around Node.js.
EDIT
After giving this some thought, and reviewing your requirements I believe there is a way to pull this off just using GAS.
You can setup a unique push notification channel for a given event per user/domain (the 'update' event in your use case) by setting the event parameter when initializing the watch. Thus the GAS web app will only be triggered if an update event occurs; you don't really need to rely on the HTTP header to determine the event type.
If you want to track multiple events, you simply create a unique channel per event and use the same GAS Web app endpoint for each one. You differentiate between events by checking the event parameter sent in the POST request. This should remove the need for middle-man services such as Heroku or Google Cloud Functions.
Had to ask this question as I couldn't find the correct way of asking Google.
I am building a browser extension that requires the user to login using his/her credentials.
Lets keep the OAuth2 way aside for a moment there. After logging in, I am storing the JWT token received from the server in the local storage.
Now when the user navigates to another website, the extension does not have access to the stored local storage data due to cross domain access restriction.
I would like to know if there is any way to maintain the session across multiple domain. Can't ask the user to keep on logging in for every other site, he/she navigates to.
Anywhere else we can store the token to make it accessible everywhere?
Edit:
For the storage via content script, have used chrome.storage.local.
On page load, the content script sets the value from the chrome.storage.local into window.localStorage, if any.
An iframe is embedded into the web page. Display none by default. Iframe does not have any URL set.
Then User clicks on the browser Action button, the iframe is displayed.
User enter the login credentials. These are captured by the script file loaded in the head section of that iframe.
Now on submit, AJAX call is made and then on its success, stores the JWT token in the storage via window.localStorage.
Here, I also want to store the same token in the chrome.storage.local so that when the page is refresh or navigated to another domain, the Step 2 from above will execute and set the window.localStorage with the same token as the previous one has. This way, user is logged in already.
Anything wrong with the above? If not, then how to send the token from the iframe to the content or background script?
I would use a background script.
Therefore add
"background": {
"scripts": [ "background.bundle.js" ],
},
to your manifest.json.
Basically a background script is like an extra browser window instance,
just running in the background concurrent to all active sites.
Here you can find an explanation how to send messages between your active sites (content script) and the background script of your application:
https://developer.chrome.com/extensions/messaging
Explaining all of this goes beyond the scope of this answer.
Basically you are interested in the "Long-lived connections" section.
You should first establish a connection between your content script and your background script. If an event (like login) occures in the content script you can use the connection to notify the background script or
vice versa.
In your background script you have an object chrome.store.local, which basically is the same as your local storage. There you can save your JWT.
Via the message passing you can persist the JWT in the background script and access it (when you navigate) through the message system in every window, and therefore it is domain independent.
Certainly you can also check if the domain is the right one in your content script.
EDIT:
As I understand your question, your problem is to get the token out of the iframe. I am not sure how your authentication in the iframe works,
but lets assume it is a form.
Then you can access the DOM of the iframe via:
my_iframe.contentWindow.document
.getElementById("myForm")
.onsubmit = function() { /* todo */ }
If you are not the creator of the iframe you should probably wrap
the onsubmit function, by something like this:
var my_form = my_iframe.contentWindow.document
.getElementById("myForm")
var old_handler = my_form.onsubmit
my_form.onsubmit = function() {
/* todo */
if(old_handler != undefined) {
var result = old_handler.apply(this, arguments)
}
/* todo (maybe send to background) */
}
Otherwise you can just delegate the event to your background / content script. Your background script can than do the authentication.
var my_form = my_iframe.contentWindow.document
.getElementById("myForm")
.onsubmit = function() {
var result = authenticate_background(arguments)
}
Here the authenticate_background sends the arguments to the background script.
Keep in mind that you cannot send dom elements to the background script.
Therefore you first must serialize the arguments.
In general I do not like the approach of using an iframe,
but if you have a given authentication provider this could be the only
viable option. Also there might be a more elegant way than wrapping
the onsubmit function. Maybe the script running in the iframe has
a special mechanism (callback) where you can register your authentication.
I am trying to run a script off of my Google Drive through the Javascript Google Drive API. This works fine, but only if I sign into my account on the popup that opens. I wish to sign into the same account every time and so was wondering if there was any way to automate the login of this so as to bypass users having to enter in that login information.
In short, you would have login at least once, everytime after the Google Identity Provider JSON Web Token expires. I am not sure how long this would be with the Goolge Drive API, but typically these tokens may be valid for anywhere from a single request to days long.
Here is the Documentation for the Google API OAuth2
https://developers.google.com/identity/protocols/OAuth2
Refresh the access token, if necessary.
Access tokens have limited lifetimes. If your application needs access
to a Google API beyond the lifetime of a single access token, it can
obtain a refresh token. A refresh token allows your application to
obtain new access tokens.
Note: Save refresh tokens in secure long-term storage and continue to
use them as long as they remain valid. Limits apply to the number of
refresh tokens that are issued per client-user combination, and per
user across all clients, and these limits are different. If your
application requests enough refresh tokens to go over one of the
limits, older refresh tokens stop working.
Google has provided a quickstart guide for implementing a user sign via Google Apis. Google uses the OAuth2 protocol in which you must register with Google as a Client application. Once registered as a Client application, you will be issued a Client ID, which you typically provide to your application in some form of application initialization.
Here is a link to their quickstart guide, which will help you get started:
https://developers.google.com/drive/v3/web/quickstart/js
Note that this is a basic example that does not demonstrate how you may approach persisting a JSON Web Token so that the user does not have to login on every request. I outline a simple approach of managing Authentication in JavaScript and Angular to get you moving in the right direction, but incomplete, direction.
For example, in Angular:
// Configures the required variables before Running an Instance of the App
angular.module("yourModuleName").config(configureApp);
AND
// Executed when the App Instance launches, allowing you to connect to Google APIs when the App starts
angular.module("yourModuleName").run(runApp);
Where configureApp and runApp are JS functions that handle application initialization in the AngularJS Framework. The code in the follow example would retrieve the Apps Google Client ID from their own App's REST API. This is just an example of where you could retrieve these credentials from storage, but most likely is not the most secure example:
var configureApp = function($http,$window) {
// Setup your CLIENT ID from your own REST API (or any other mechanism you may choose)
var httpPromise = $http.get("http://myApp.myDomain.com/config/googleClient");
// Handle the Response from the above GET Request
httpPromise.then(
// Handle Success
function(response) {
// Store the CLIENT ID in local storage for example
$window.localStorage.setItem("GOOGLE_API_CLIENT_ID", response.data.clientId);
// Setup the App Wide variables for Google API
// Client ID and API key from the Developer Console
var CLIENT_ID = response.data.clientId;
// Array of API discovery doc URLs for APIs used by the quickstart
var DISCOVERY_DOCS = ["https://www.googleapis.com/discovery/v1/apis/drive/v3/rest"];
// Authorization scopes required by the API; multiple scopes can be
// included, separated by spaces.
var SCOPES = 'https://www.googleapis.com/auth/drive.metadata.readonly';
// Do more initialization configuration
};
var runApp = function() {
// Initialize the API
gapi.client.init({
discoveryDocs: DISCOVERY_DOCS,
clientId: CLIENT_ID,
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;
});
}
Which function to use with Angular would depend on the desired app lifecycle you need to target in an Angularjs app. This approach can be applied in other JS frameworks like React and Backbone.
To highlight another perspective from the documentation, updateSigninStatus would be a great place to capture the JSON Web Token returned by Google's Authorization request at which point you could store this token in the browser's window.localStorage for re-use.
You then could reuse the token whenever the Google API requires authentication. Tokens typically have an expiration. Until the token expires, you would be able to prevent the API from displaying a login modal.
This does mean you would still have to manage the logic behind the Authorization process using this approach, monitoring any response from Google requesting a token refresh or re-authentication.
Auth0 is a great Authentication and Authorization plugin available in many languages for connecting with Google and many other OAuth2 Identity Providers. The Google Drive API uses their own Identity Provider Service to confirm the Identity of your apps users in tandem with your registered app's Client ID.
Here are links that I found when implementing Authorization for a project that required me to implement Authorization using the Google Identity Provider:
https://jwt.io/
https://auth0.com/
Best practices for authentication and authorization in Angular without breaking RESTful principles?
https://thinkster.io/tutorials/angularjs-jwt-auth
You are saying that all users login to the same Google account?
In that case you have 2 options.
1/ write a server application that has a stored refresh token. Create an endpoint that allows an authenticated user to request an access token.
2/ embed a refresh token in your JavaScript, but make sure that only authenticated users can load the JS
I'm trying to develop a Chrome extension on which people can sign in with their Twitter account (using OAuth 1.0A).
I have not provided the callback URL in the Twitter application settings so far, so the user is returned a PIN to enter on the extension to complete the authorization process. But I don't want to implement the final PIN verification step in the extension.
Instead, I'd like that once the user was redirected to Twitter and authorized the app to access his account, he is automatically logged into the extension and can use it in order to post tweets.
I know I should definee a callback URL but I have no idea on what to use. I've been struggling for several days, during which I tried multiple things (like chrome://extensions/extension_id/ but it's not accepted by Twitter). I didn't manage to find the solution.
Any idea on what should be the callback URL? And what should contain the file pointed by that URL?
I know this is incredibly old, but I had the same issue and finally figured out how to do it, so hopefully this helps other people who find this.
In your twitter apps configuration, you have to add a callback URL to a domain. Then, in your chrome extension, you have to have a content script that is injected on that same domain. The content script will have some logic that will parse the query string for the auth tokens and send that as a message to your background script. Then, in your background script, you can use those tokens to get the actual oauth tokens, which are then saved to local storage or chrome.storage.
Here is a brief example:
in your manifest.json, make sure your content script matches the same domain that you put in your twitter app settings callback URL:
"content_scripts": [{
"matches": ["https://andyjiang.com/*"],
"js": ["js/session.js"]
}]
Then, in your js/session.js file, have this sort of logic:
chrome.runtime.sendMessage({type: 'auth', session:
window.location.search.substr(1)}, function(response) {
window.open('', '_self', '');
window.close();
});
In your background script, have some logic that listens for the message, gets the token, and uses Twitter's API for the third leg of the oauth process to finally get the oauth token and oauth token secret, which you can then save in localStorage or chrome.storage.
Hope that helps. Here is sample code if you want to play around with it.
https://github.com/lambtron/chrome-extension-twitter-oauth-example
I'm playing around with AppScript and try to get an oAuth 2.0 access token.
Any sample out there how to get this working in AppScript?
I am working on a cleaner tutorialized version of this, but here is a simple Gist that should give you some sample code on how things would work -
https://gist.github.com/4079885
It still lacks logout, error handling and the refresh_token capability, but at least you should be able to log in and call a oAuth 2 protected Google API (in this case its a profile API).
You can see it in action here -
https://script.google.com/macros/s/AKfycby3gHf7vlIsfOOa9C27z9kVE79DybcuJHtEnNZqT5G8LumszQG3/exec
The key is to use oAuth 2 Web Server flow. Take a look at getAndStoreAccessToken function in the gist to get the key details.
I hope to have this published in the next few weeks but hopefully this will help in the mean time.
UPDATE - adding in info on redirect_uri
The client secret is tied to specific redirect URIs that the authorization code is returned to.
You need to set that at - https://code.google.com/apis/console/
The highlighted URI needs to match the published URI (ends in /exec). You get the published URI from the script editor under Publish -> Deploy as web app. Make sure you are saving new versions and publishing the new versions when you make changes (the published URI stays the same).
I've modified the example above to use the newish state token API and the CacheService instead of UserProperties, which is now deprecated. Using the state token API seems to make things a little more secure, as the callback url will stop accepting a state token after a timeout.
The same caveats apply. Your redirect URIs have to be added to your (script) project in the developer's console, meanwhile you have to yank the CLIENT_SECRET and CLIENT_ID from the console and paste them in. If you're working within a domain, there don't seem to be any guarantees on what URL will be returned by ScriptApp.getService().getUrl(), so I wound up basically having it get the address dynamically, then waiting for to fail on the the (second) redirect, and then hard-coded the resulting URI.
https://gist.github.com/mclaughta/2f4af6f14d6aeadb7611
Note that you can build an OAuth2 flow using this new API, but it's not a complete sample yet:
https://developers.google.com/apps-script/reference/script/script-app#newStateToken()
In particular, you should not pass 'state' directly to the /usercallback URL yourself, because the OAuth2 service provider is responsible for round-tripping the 'state' parameter. (Instead, you pass 'state' to the auth URL, and the service provider automatically attaches it to the callback URL.)