EzyOauth2 patterns


EzyOauth2 has now been superseded by Goa, so this document is for legacy information. For more information see OAuth2 for Apps Script in a few lines of code. If you are using EzyOauth2, it's easy to migrate. If you are starting up, consider using Goa instead - it's easier and has more features.

In EzyOauth2 - taking some pain out of Apps Script API authentication, i provided a library to simplify oAuth2. In this pots, I'll show some patterns you can use to further simplify the use of oAuth2 with Google Apps Script. The reason I provide these as patterns, rather than as a library, is to protect your credentials - in other words either create your own pattern library, or copy the patterns to your own scripts. These patterns should be able to be used pretty much without modification. 

For other authentication packages, see 
only a few minor modifications are needed from the Google version shown below.

Getting started

As described in EzyOauth2 - taking some pain out of Apps Script API authentication, you need to set up your application in the developers cloud console and get some credentials. The idea here is that you store these in your user properties. 


Pattern 1 - Storing credentials in your user properties. 

Create a separate script in your current project, and add this code, substituting your own credentials and scopes. Once you have finished testing, I recommend that you delete this script, or keep it in a separate script. This only needs to be run once for a project, except where you want to clear out any token information. You'll notice that it asks if you want to allow offline access in the first time authentication dialog. This is needed to allow the automatic of refreshment of tokens in future passes without the need to repeat the conversation.

/* getting and setting properties and tokens
 * get your authentication package from your property service
 * return {object} an authenticatin package that looks like the below and comes from the google cloud console for your projects
 * 
 * clientId : "xxxxxxxx.apps.googleusercontent.com",
 * clientSecret : "xxxxxxxxxxx",
 * scopes : ['https://www.googleapis.com/auth/datastore','https://www.googleapis.com/auth/userinfo.email'] // example
 * the same object will be updated to contain refresh tokens, and current access tokens
 * note that the first time you run it, it will fail unless you've already set up the cloud console url
 * copy the callback url it shows to your cloud console and you'll be good to go 
 */

/**
 * setting your user properties, one time, with your credentials
 * running this will provoke an oauth dialog, since any refresh tokens will be cleared out
 * once you have run this once, you can delete it from this file.
 * @return {void}
 */
function oneTimeSetProperties () {
  setAuthenticationPackage_ ({ 
    clientId : "1xxxxxxp0c3huvum.apps.googleusercontent.com",
    clientSecret : "ejiy4e0xxxx",
    scopes : ['https://www.googleapis.com/auth/datastore','https://www.googleapis.com/auth/userinfo.email'],
    service: 'google'
  });
}

Pattern 2 - a web application.

A typical web application, with a doGet() would look like this, using the patterns I'll show later in this article. Create script that looks like this

/** 
 * this is your web app
 * @param {object} webapp param object
 * return {HtmlOutput} 
 */
function doGet (e) {
  return doGetPattern(e, constructConsentScreen, doSomething) ;
}

/**
 * tailor your consent screen with an html template
 * @param {string} consentUrl the url to click to provide user consent
 * @return {string} the html for the consent screen
 */
function constructConsentScreen (consentUrl) {
  return '<a href = "' + consentUrl + '">Authenticate to datastore</a> ';
}

/**
 * this is your main processing - will be called with your access token
 * @param {string} accessToken - the accessToken
 */
function doSomething (accessToken) {
 
   var options = {
     method: "POST",
     headers: {
       authorization: "Bearer " + accessToken
     }
   };

  var result = UrlFetchApp.fetch("https://www.googleapis.com/datastore/v1beta2/datasets/xliberationdatastore/lookup", options);
  return HtmlService.createHtmlOutput (' it worked' + result.getContentText());

}

/**
 * gets the property key against which you want the authentication package stored
 * @return {void}
 */
function getPropertyKey_ () {
  return "EzyOauth2Datastore";
}

walkthrough

doGetPattern(e, constructConsentScreen, doSomething) 

Your normal doGet() is going to first be passed through a process that will either 
  1. Find a current access token in your property store
  2. Use a refresh token to get a new access token in place of an expired one
  3. Take you through a user consent dialog, and store the token information in your properties. It will need you to have created a constructConsentScreen() function (sample shown above), of what you would like your consent screen to look like. Note that, as described in EzyOauth2 - taking some pain out of Apps Script API authentication, you'll need to register your callback url in the Google Cloud console. It will be something like the below. Alternatively you can just run this, let it fail, and copy the callback url from the error message to the cloud console.
https://script.google.com/a/macros/xxxx.com/s/xxxxx/usercallback

Once one of these has been executed, your function doSomething (example above), will be called with an accessToken as the argument, so you can process you doGet() call as normal. 

You also need to provide this function, which returns the name of the property key it should use to store token information. The package name is for a later article. It is to allow multiple packages in the same project so you attach to multiple services (google, linkedin etc) at the same time.
/**
 * gets the property key against which you want the authentication package stored
 * @param {string} optPackageName
 * @return {string}
 */
function getPropertyKey_ (optPackageName) {
  return "EzyOauth2Datastore" + (optPackageName ? '_' + optPackageName : '');
}

Non Web app approach

Even though your app is not a web app, you'll still need to publish it. This is so that the call back authentication dialog can take place at least once. From then on the refreshing of access tokens is automatic and doesn't need a web app. The only difference then, is that a non web app pattern looks like this

/**
 * this is assuming that you have already authorized this script as a web app
 * and that the access token/refresh token package is already stored
 * @return {*} whatever your doTheWork function returns
 */
function dataStoreExample() {
  // this will get an access token and pass it to doTheWork()
  return doGetPattern({} , constructConsentScreen, doTheWork);
}
/**
 * this is your main processing - will be called with your access token
 * @param {string} accessToken - the accessToken
 */
function doTheWork (accessToken) {
   var options = {
     method: "POST",
     headers: {
       authorization: "Bearer " + accessToken
     }
   };
  var result = UrlFetchApp.fetch("https://www.googleapis.com/datastore/v1beta2/datasets/xliberationdatastore/lookup", options);
  return HtmlService.createHtmlOutput (' it worked' + result.getContentText());
}

The pattern script

This script can be copied as is into your project - and of course you will need the EzyOauth2 library (MSaYlTXSVk7FAqpHNCcqBv6i_d-phDA33)

/* 
 * patterns you can reuse for writing apps needing oAuth2
 * just copy the whole thing to your project 
 * for first time running seee oneTimeSet to load your credentials to your property store
 * you shouldn't need to modify any of this
 */


/**
 * gets the property key against which the authentication package will be stored
 * @param {string} optPackageName
 * @return {object} authentication package
 */
function getAuthenticationPackage_ (optPackageName) {
  var p = PropertiesService.getScriptProperties().getProperty(getPropertyKey_ (optPackageName));
  if (p) { 
    p.packageName = optPackageName || '';
  }
  return p ? JSON.parse(p) : null;
}

/**
 * set your authentication package back to your property service
 * this will make the access token and refresh token available next time it runs 
 * @param {object} authentication package to set
 * @return {void}
 */
function setAuthenticationPackage_ (package) {
  PropertiesService.getScriptProperties().setProperty(getPropertyKey_ (package.packageName), JSON.stringify(package));
}

/** 
 * this will be the first call back, you now need to get the access token
 * @param {object} e arguments as setup by the statetokenbuilder
 * @param {function} theWork that will be called with the access token as an argumment
 * @return {*} the result of the call to func()
 */
function getAccessTokenCallback(e) {

  // this will fetch the access token
  var authenticationPackage = getAuthenticationPackage_ (e.parameter.package_name);
  var eo = new cEzyOauth2.EzyOauth2 (authenticationPackage).fetchAccessToken(e);
  
  if (!eo.isOk()) {
    throw ('failed to get access token:'+eo.getAccessTokenResult().getContentText());
  }
  else {
    // should save the updated properties for next time
    setAuthenticationPackage_ (authenticationPackage);
    return e.parameter.work ? evalWork(e.parameter.work) : null;
  }
      
  function evalWork (func) {
    return eval (func +'("' +eo.getAccessToken() +'")');
  }
}

/**
 * gets called by doGet
 * @param {object} the doGet() parameters
 * @param {function} consentScreen - will be called with the consent Url as a an argument if required
 * @param {function} doSomething - the function that actually does your work
 * @param {function} optPackageName - optional package name to identify the oauth2 package to use
 * @return {*} whatever doSomething returns
 */ 
function doGetPattern(e, consentScreen, theWork,optPackageName) {
  // set up authentication
  var packageName = optPackageName || '';
  var authenticationPackage = getAuthenticationPackage_ (packageName);
  if (!authenticationPackage) {
    throw "You need to set up your credentials one time";
  }

  var eo = new cEzyOauth2.EzyOauth2 ( authenticationPackage, "getAccessTokenCallback", undefined, {work:theWork.name,package_name:packageName} );
  
  // eo will have checked for an unexpired access code, or got a new one with a refresh code if it was possible, and we'll already have it
  if (eo.isOk()) {
    // should save the updated properties for next time
    setAuthenticationPackage_ (authenticationPackage);
    // good to do whatever we're here to do
    return theWork (eo.getAccessToken());
  }
  
  else {

    // start off the oauth2 dance - you'll want to pretty this up probably
      return HtmlService.createHtmlOutput ( consentScreen(eo.getUserConsentUrl()) );
  }
}

Your properties

If you want to see what get stored in your properties, take a look at your project properties against the key you supplied in your code. You'll see something like this. The refresh token is used to get a new access token when the current one expires.

{
    "clientId": "10xxxxxxxxxxxxhuvum.apps.googleusercontent.com",
    "clientSecret": "ejxxxxxxxxxxxxxxxxx",
    "scopes": [
        "https://www.googleapis.com/auth/datastore",
        "https://www.googleapis.com/auth/userinfo.email"
    ],
    "service": "google",
    "access": {
        "accessToken": "ya29.OAChxTghzpvxxxxxxxxxxxxxxxxxx",
        "refreshToken": "1/ZVYvzt1xxxxxxxxxxxxxxxxxxxM",
        "expires": 1404465829384
    }
}

Summary

This approach takes away the complication of oAuth2. Once your credentials are set up, all you have to do is pass the function that does the work to doGetPattern(e, constructConsentScreen, doSomething)   and it will be called with a fresh accessCode. Simple!
Comments