Using OAUTH2 with a webapp published 'as me'

In Borrowing an access token from Apps Scripts I demonstrated the simplest way to get a token to use with a Google API, by borrowing one from Apps Script. Next up, was Using a service account where could generate a token by knowing the appropriate credentials for project. This was used in the context of accessing resources belonging to a project.

Now it gets a little more difficult, as we need to introduce interaction with the user (the person accessing the webapp), since we now need to get permission to access resources owned by that person. There are really two kinds of scenarios.
  • The resources belong to me (the script owner) and I want the web app to access them as me  - thus avoiding giving permission to those resources to others (users) that are using the webapp. An example of this could be some files on Drive belonging to  me.
  • The resources belong to the webapp user. This user has to give the script permission to access their resources. An example might be to process some files on the Drive of the user.
This post will deal with the first. An app that accesses some files on drive belonging to me. I'll deal with the other scenario in a separate post (although the mechanism is very similar). 

For this example, we'll use my cGoa library, as described in  OAuth2 for Apps Script in a few lines of code 

All the demo code is available on github. Note that I'm using the same code for all these examples, so the final source on github will have evolved in some details from the writeup here.

Quick summary of all OAuth2 scenarios


If you prefer there is a simplified video version of this post

The example

I'll develop the cloud vision example I used in Using a service account. In that example, there was no interactivity. The goa library took care of getting the tokens. All that was needed was the credentials. 

In this web version, I want to allow users to select from files on my Drive, and indicate what to do with them. I'm not going to give these users access to my drive. I'm simply going to allow them to select from a couple of directories belonging to me.

Here's what the initial screen looks like.

And here's the result



In reality, for an example like this, a service account may be better - because the only person who could authorize access to these Drive resources is me - and in fact I don't want an authorization dialog to happen to anyone other than me. That's why in this instance, once an authorization have been given, Goa works like a service account and won't prompt for authorization beyond the initial one.

I'll use the same data and functions as  in Using a service account to access the Cloud Vision API.

Creating credentials

I'll be using the same project  as in Using a service account. The scope is the same - and all I have to do is to add some credentials for a web application in the developers console, like this.


and then

We can leave the restrictions section blank for now, and don't worry about noting the client id and client secret. We'll download those later. 

Storing the credentials.

The best way to do this is to download the credential file, since Goa knows how to read that kind of file.

Just like in Using a service account (which uses the same scope as well), we need to run a once off function to store these credentials. You'll just need to provide the id of the file you just downloaded.
  // web account for cloud vision - taken from downloaded credentials
  cGoa.GoaApp.setPackage (propertyStore , 
    cGoa.GoaApp.createPackageFromFile (DriveApp , {
      packageName: 'cloudvision',
      fileId:'0BxxxxxxxxxxxxxQ',
      scopes : cGoa.GoaApp.scopesGoogleExpand (['cloud-platform']),
      service:'google'
    }));

Enabling the API

If you've done the Using a service account example, the cloudvision API will already be enabled. 

Include cGoa

You'll need the cGoa library to manage the interactions with the Google token infrastructure. It's here

MZx5DzNPsYjVyZaR67xXJQai_d-phDA33

or if you prefer, you can get the source from github.

The webApp

The entire code for the web app is starting to get a bit long to publish here, but all the examples are on github. I'll just focus on the key points in this post. 

To be able to publish a web app, you need a function called doGet. Here's what a typical doGet for a webApp will look like when using Goa.
/**
 * this is how  to do a webapp which needs authentication
 * @param {*} e - parameters passed to doGet
 * @return {HtmlOurput} for rendering
 */
var HTML_FILE = 'asme' // or 'asanother'
function doGet (e) {
  
  // this is pattern for a WebApp.
  // passing the doGet parameters (or anything else)
  // will ensure they are preservered during the multiple oauth2 processes
      
  // get the goa for this project
  var goa = cGoa.GoaApp.createGoa(
    'cloudvision', 
    PropertiesService.getScriptProperties()
  ).execute(e);
  
  // it's possible that we need consent - this will cause a consent dialog
  if (goa.needsConsent()) {
    return goa.getConsent();
  }
  
  // if we get here its time for your webapp to run 
  // and we should have a token, or thrown an error somewhere
  if (!goa.hasToken()) { 
    throw 'something went wrong with goa - did you check if consent was needed?';
  }

  // now return the evaluated page
  return HtmlService
  .createTemplateFromFile(HTML_FILE)
  .evaluate()
  .setSandboxMode(HtmlService.SandboxMode.IFRAME)
  .setTitle('Web app Oauth2 ' + HTML_FILE)

} 

Webapp walkthrough

I'm using the same code for this webapp (run as me), as I will use for the next, more enhanced version (run as the user). The html will be different though, so I have two html templates. This example uses asme.html
  
  var HTML_FILE = 'asme' // or 'asanother'

Every app needs to start by getting  and executing an instance of goa object, using the package name that matches the one against which the credentials were stored, and also a properties store to use to maintain the token infrastructure. Since I'm publishing this as me, I want anyone using the app to use my token - so I use the ScriptProperties store.
  
  // get the goa for this project
  var goa = cGoa.GoaApp.createGoa(
    'cloudvision', 
    PropertiesService.getScriptProperties()
  ).execute(e);

Some OAuth2 implementations require you to manage callbacks to deal with the case where an authorization dialog is required, and since the webapp might need to restart a couple of times this can be a little complicated. However, Goa allows you to just write the code sequentially, and it will restart itself at this point as many times as it needs. needsConsent() will return true if this is the first time this app has been run, otherwise it will just pass through. getConsent() is able to restart the app and re-enter this function, maintaining the state between re-entrances. 

  // it's possible that we need consent - this will cause a consent dialog
  if (goa.needsConsent()) {
    return goa.getConsent();
  }

The below should never be true, since the only time we get to here is if Goa has received the necessary consent to continue and has successfully initialized the OAUTH2 process.

  // if we get here its time for your webapp to run 
  // and we should have a token, or thrown an error somewhere
  if (!goa.hasToken()) { 
    throw 'something went wrong with goa - did you check if consent was needed?';
  }

Now we have an usuable token, and can go on and generate an app that uses it.

  // now return the evaluated page
  return HtmlService
  .createTemplateFromFile(HTML_FILE)
  .evaluate()
  .setSandboxMode(HtmlService.SandboxMode.IFRAME)
  .setTitle('Web app Oauth2 ' + HTML_FILE)

Some test Data.

You need to create a folder with some images in it to play around with. Alternatively, you can copy mine

Asme.html will need which folder ids to expose, so modify the code below with your folder ids.
  <div class="block">
    <label for="folderid">Folder id containing images</label>
    <select id="folderid">
    <option value="0B92ExLh4POiZYzZZT3d0aU9VV1U" selected>Pets</option>
    <option value="0B92ExLh4POiZZDNKcVN0QTJJMGs">Faces</option>
    </select>
  </div> 

Publishing the webapp

Once you have all the rest of the code from github or taken a copy of my project, you'll be able to publish to 'run as me', and make it public (Btw - I don't recommend you do this using the cloudvision API as it's one you need to pay for using!). As usual, first you should run doGet locally just to provide the regular script authorization before publishing. 


When you run the Webapp for the first time, you'll get this consent dialog, which will have been generated by goa.needsConsent()

You need to copy the redirect URI, go back to the developer console, and tell it where to expect requests from in the Authorized redirect URI section. You also need to enter http://script.google.com in the authorized JavaScript origins section. This is a security feature which prevents unknown projects using your console project. 


Now you can tell it to go, and you'll go through some kind of authorization dialog. Your app will then get called back from this dialog, but Goa will handle this and have a background conversation to obtain an access token. Eventually you'll pass through this code...

  // it's possible that we need consent - this will cause a consent dialog
  if (goa.needsConsent()) {
    return goa.getConsent();
  }

and your web app will start up...


Retreiving the access token

The token can be retrieved at any time by asking Goa for it. It's better to do this any time you need it, rather than to store it in some global variable. This is because Goa will refresh it automatically if it has expired in the meantime. Although this is probably unlikely in a Server side scenario (Goa always ensures a token has at least 6 minutes on it to guarantee it will exceed the server side script run time quota), client side apps could be active for any amount of time. Here's a namespace I use in this app to retrieve the token for me when I need it.
/**
 * namespace to deal with oauth
 * @namespace Oauth
 */
var Goth = (function(ns) {
  
  /**
   * get a goa or fail
   * @param {string} packageName the package name
   * @param {PropertiesService} [props=ScriptProperties]
   * @return {Goa} a goa
   */
  ns.getGoa = function (packageName,props) {

    return cGoa.GoaApp.createGoa(
      packageName , 
      props || PropertiesService.getScriptProperties()
    ).execute();

  }
  
  /**
   * get a token or fail
   * @param {string} [packageName=cloudvision] the package name
   * @param {PropertiesService} [props=ScriptProperties]
   * @return {Goa} a goa
   */
  ns.getToken = function (packageName,props) {

    var goa = ns.getGoa (packageName , props);
  
    if (!goa.hasToken()) {
      throw 'no token retrieved';
    }
    return goa.getToken();
  }
  
  return ns;
}) (Goth || {});

It's used like this

  ns.provoke = function (package) {
    return Pull.getDataFor ( Goth.getToken('cloudvision'), package);
  };


For more like this, see Google Apps Scripts snippets. Why not join our forumfollow the blog or follow me on twitter to ensure you get updates when they are available. 

You want to learn Google Apps Script?

Learning Apps Script, (and transitioning from VBA) are covered comprehensively in my my book, Going Gas - from VBA to Apps script, All formats are available from O'ReillyAmazon and all good bookshops. You can also read a preview on O'Reilly

If you prefer Video style learning I also have two courses available. also published by O'Reilly.
Google Apps Script for Developers and Google Apps Script for Beginners.


Comments