EzyOauth2 - taking some pain out of Apps Script API authentication

If you are like me, you will dread projects that involve oAuth2. It never seems to work first time and it's fiddly. It's also irritating that UrlFetchApp gives you the impression (false hope) that it's going to help you with oAuth, but forgets to mention that it's talking about oAuth, not oAuth2. 

If you are writing webapps and accessing some of the Google JSON APIs services, you're going to need to deal with oAuth2. I decided that I was fed up with it, and provide here a library that should help get it right first time. EzyOauth2 prefers you to store access and refresh tokens it obtains, probably in your propertiesService store. That way, it should only take you through the authorization dialogue once, since it automatically refreshes tokens when they expire. You'll find  example application patterns here.

Quickstart

getting started with cezyoauth2



The cloud console.

I'm assuming that if you are here, you'll know about the cloud console.  First you need to go there and create a project, and some credentials. 


Ignore the redirect URIs for now, we'll fix that later. 

Find out which scopes you'll need for your project

I'm creating a project to access the cloud DataStore, so I need these scopes.

scopes : ['https://www.googleapis.com/auth/datastore','https://www.googleapis.com/auth/userinfo.email']

I find that the Oauth2 playground is the best place to find out what you need for each API

Create your webapp

Now create a new blank script. 

We're going to first of all store the credentials we just created in its script properties to keep them secret. Use the code below, substituting your credentials and scopes, then run initialStore().  You should use whichever property store is most appropriate. I'm using ScriptProperties, but UserProperties might be more appropriate, depending on your objectives, but that's up to you.

var PROPERTY_KEY = "EzyOauth2Datastore";

function initialStore () {
  setAuthenticationPackage ( {
   clientId : "xxxxxxxx.apps.googleusercontent.com",
   clientSecret : "xxxxxxxxxxx",
   scopes : ['https://www.googleapis.com/auth/datastore','https://www.googleapis.com/auth/userinfo.email']
  });
}

function setAuthenticationPackage (package) {
  PropertiesService.getScriptProperties().setProperty(PROPERTY_KEY, JSON.stringify(package));
}

You can check it worked by looking at your project properties. If it did you can delete all that again, since you now have your credentials safely stored.

Add the EzyOauth2 library.

add a reference to this library

MSaYlTXSVk7FAqpHNCcqBv6i_d-phDA33

Discover your redirectURI, and update the cloud console

In order that it can check that you are really you, accessing the service from where you intended, you have to enter a redirectURI into the cloud console. Copy in the code below - and publish your web app.

function doGet(e) {
  var eo = new cEzyOauth2.EzyOauth2 ({});
  return HtmlService.createHtmlOutput ( 'enter this as your redirect uri into the cloud console for this project ' + eo.showRedirect());
}

You'll get something like this, and you'll copy the url into the redirect URIs section of the cloud console.

enter this as your redirect uri into the cloud console for this project https://script.google.com/a/macros/mcpher.com/s/AKfycbx8eCE2V-weu2TPslW4LdVqEPJ7porBfxaw4WdIV70/usercallback

Write your app

The model below shows how to write an app using EzyOauth2. This example is preparing for writing to the cloud DataStore. The first time you run it, it will take you through the regular oAuth2 dialog. Subsequently it will use refresh tokens to automatically update your access token.  There's really not much to it, and you probably don't need to change it.



/**
 * 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
 * 
 */

var PROPERTY_KEY = "EzyOauth2Datastore";


function getAuthenticationPackage () {
  var p = PropertiesService.getScriptProperties().getProperty(PROPERTY_KEY);
  return p ? JSON.parse(p) : {};
}

/**
 * set your authentication package back to your property service
 * this will make the access token and refresh token available next time it runs 
 */
 
function setAuthenticationPackage (package) {
  PropertiesService.getScriptProperties().setProperty(PROPERTY_KEY, JSON.stringify(package));
}

// this is your web app

function doGet (e) {

  // set up authentication
  var authenticationPackage = getAuthenticationPackage();
  var eo = new cEzyOauth2.EzyOauth2 ( authenticationPackage, "getAccessTokenCallback" );
  
  
  // 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 doSomething (eo.getAccessToken());
  }
  
  else {

    // start off the oauth2 dance - you'll want to pretty this up probably
      return HtmlService.createHtmlOutput ( '<a href = "' + eo.getUserConsentUrl() + '">Authenticate to datastore</a> ');
  }
}

/** 
 * this will be the first call back, you now need to get the access token
 * @param {object} e arguments as setup by the statetokenbuilder
 * @return {void}
 */
 
function getAccessTokenCallback(e) {

  // this will fetch the access token
    var authenticationPackage = getAuthenticationPackage();
    var eo = new cEzyOauth2.EzyOauth2 (authenticationPackage).fetchAccessToken(e);

    if (!eo.isOk()) {
      throw ('failed to get access token:'+eo.getAccessTokenResult().getContentText());
    }
    else {
    // all is good we can go ahead and do something
     
     // should save the updated properties for next time
      setAuthenticationPackage(authenticationPackage);
      return doSomething (eo.getAccessToken());
    }

}

// this is your processing - all is now normal
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());

}

Walkthrough

When your web app is kicked off it will run doGet(). The only things you might need to change here is

doSomething (eo.getAccessToken());

The doSomething() function is the one that is eventually called when an access token is obtained. It will get called from here if there is no authorization dialog required - meaning that EzyOauth2 was able to get or refresh an access token without provoking dialogue. Your doSomething() function is the one that does the work of this webapp. It will be passed the access token.

If an authentication dialogue is required, this is how it is displayed to the user. You may want to pretty this up a bit.
return HtmlService.createHtmlOutput ( '<a href = "' + eo.getUserConsentUrl() + '">Authenticate to datastore</a> ');

Code for doGet()

function doGet (e) {

  // set up authentication
  var authenticationPackage = getAuthenticationPackage();
  var eo = new cEzyOauth2.EzyOauth2 ( authenticationPackage, "getAccessTokenCallback" );
  
  
  // 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
    doSomething (eo.getAccessToken());
  }
  
  else {

    // start off the oauth2 dance - you'll want to pretty this up probably
    
      return HtmlService.createHtmlOutput ( '<a href = "' + eo.getUserConsentUrl() + '">Authenticate to datastore</a> ');
    }
    
  }
}

Callback


This function gets called after the user consent dialog, if one was needed. You probably don't need to change any of this.
function getAccessTokenCallback(e) {

  // this will fetch the access token
    var authenticationPackage = getAuthenticationPackage();
    var eo = new cEzyOauth2.EzyOauth2 (authenticationPackage).fetchAccessToken(e);

    if (!eo.isOk()) {
      throw ('failed to get access token:'+eo.getAccessTokenResult().getContentText());
    }
    else {
    // all is good we can go ahead and do something
     
     // should save the updated properties for next time
      setAuthenticationPackage(authenticationPackage);
      return doSomething (eo.getAccessToken());
    }

}

doSomething

this is where you start your work. The access code will be passed to it. In this case, I'm doing a cloud datastore lookup
// this is your processing - all is now normal
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());

}

The library code
here is the code for the EzyOauth2 library. You can include it in your project by referencing MSaYlTXSVk7FAqpHNCcqBv6i_d-phDA33, or fork it by copying https://script.google.com/d/1lW9pn80yQH1hbbKsZDJiZTtvioJw8MWppFj8G3FBz7BegvhOSSI6pNYf/edit?usp=sharing


For help and more information join our forum,follow the blog or follow me on twitter .

Comments