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.
If you prefer there is a simplified video version of this post
https://youtu.be/x-91v1s3vws
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.htmlvar 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.Lorem ipsum dolor sit amet, consectetur adipiscing elit. Ut elit tellus, luctus nec ullamcorper mattis, pulvinar dapibus leo.
<label for="folderid">Folder id containing images</label> <select id="folderid"> <option value="0B92ExLh4POiZYzZZT3d0aU9VV1U" selected>Pets</option> <option value="0B92ExLh4POiZZDNKcVN0QTJJMGs">Faces</option> </select>
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…
Retrieving 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); };
Why not join our forum, follow the blog or follow me on Twitter to ensure you get updates when they are available.