Demystifying oAuth and ArcGIS

Posted by DaveBouwman.com on August 27, 2017

Over the years I’ve talked with a lot of developers who find oAuth to be mysterious, and really, when we watch what’s happening “on-the-wire”, it’s not too complicated. In this post we will review how it works, and we will implement it in ~120 lines of commented javascript. Let’s jump in!

What is oAuth?

Let’s start by recalling the web before oAuth - where you had a login at every different site… and of course you used good security practices and had a different password for every site… (me neither). So, while painful to manage, it was theoretically “ok”. But then we started building applications that integrated with other systems. Now we have a problem - how can application A take actions in system B on a user’s behalf without having the users credentials?

This is the problem that oAuth solves - it allows applications to use a third-party system for authentication, as well as managing access to services provided by that system.

Three Components

oAuth provides three main things:

Identity

We see “Sign In with Facebook” buttons on all manner of sites - from Strava to CodePen, millions of applications now leverage the fact that 3+ billion of us have Facebook accounts, and none of us want to create “yet-another-account”. Via oAuth, we can use our Facebook identity on other sites.

Privileges

When you use an oAuth provider to access a site, it will usually ask for some sort of privilege, allowing it to act on your behalf - i.e. Post to your Facebook timeline, or store items in your ArcGIS Online organization. For Facebook there are a wide range of grants that can be requests, but for ArcGIS Online it’s all or nothing, which makes things simpler for us.

Revokability

Should you decide you no longer want the application to have access to your account, you can revoke access at the origin - i.e. at Facebook - instead of having to find the site, login, and flail around trying to delete your account. In my mind, this is a huge feature.

ArcGIS oAuth

Similar to Google/Facebook/GitHub and others, ArcGIS Online / Portal / Enterprise support authentication via oAuth - specifically oAuth 2. This article will focus on the “web flow” (also known as the ‘implicit grant’), which is specific to web applications - if you are working with a desktop or native application you should use the “authorization code grant” flow. Documentation here

The Steps

Before talking about code, let’s review the basic steps.

Application Boots

Your application loads in a browser, and shows the user a screen saying they need to sign-in in order to do amazing things.

User Clicks Sign-In button

The application constructs a url to the ArcGIS Online Authorize login page, and it either opens a pop-up window (what we will do), or redirects the entire browser to this url.

This url contains two key pieces of information - a client_id and a redirect_uri. More on these later, but they basically tell ArcGIS Online what application the user is authenticating with.

User Provides Credentials to ArcGIS Login Screen

This is the key part - the user provides their credentials to a page hosted by ArcGIS Online. Your super-cool application never has access to their credentials.

User Submits Credentials, and ArcGIS Online Approves access

A number of things happen at this point - and I think this is where it seems somewhat magical… but bear with me - it’s not that complex:

  • the login form is POSTed to ArcGIS Online
  • assuming the username & password checkout, it returns a page asking you to allow the app access to your account
  • clicking that button then POSTs another form back to ArcGIS Online, which then responds with a page that has a meta tag that redirects the browse… to the redirect_uri we passed in.
  • The browser then redirects to that page, which loads up, and passes the url to your application

See that’s not too bad?

Secret Sauce…

So the really crafty bit here is that the redirect_uri that ArcGIS Online returns to the browser is a hashed-url. This means that the url has a # in it - followed by the access token. Now, the cool thing about browsers is that they don’t send the hash or anything following that to the server.

The browser is told to load http://yourcoolapp.com/redirect.html#token=SEEKRET&username=joeuser, but the only part that goes over the wire is http://yourcoolapp.com/redirect.html. Once that page is loaded, we can access the entire url (including the hash) in javascript… and thus get the token. Since the authorize page @ ArcGIS is loaded over https, and the hash does not go over the wire, we can have a secured authentication system even when our app runs on http. That said - be sure that all requests sent with a token use https!

The Code

This demo app is about as stripped down as it can get and still be readable and have all the features. Please note - this is not production code! My point here is to strip back the mysteries so we can see what’s happening. The general process will work for any application, but when building a real application, you’d want to work with an established session management system for whatever framework you are using - i.e. torii for EmberJs etc.

Features

  • allow a user to sign-in
  • use localStorage to persist authentication info so they can be automatically logged in if they return to the app before their token expires
  • allow a user to sign out, which clears their current session and removes entry in localStorage
  • when a user is authenticated, show some information about them from their AGO user.

Step 1: Registering an Application with ArcGIS Online

Sign in at the ArcGIS for Developers site, and hit the plus icon at the top, and create a “New Application”. Fill in the basic information, and then go to the “Authentication” tab and leave it open. You will need the client_id and once you host your app somewhere, you will need to enter some valid redirect_uris. You can start with http://localhost:8080/redirect.html if you cloned and are running locally with http-server (see below)

Step 2: Clone the Repo and Install Dependencies or View the Demo App

The repo for this example is at https://github.com/dbouwman/vanilla-arcgis-oauth - you can either view it on github or clone it and follow along like that. The demo site is at http://vanilla-arcgis-oauth.surge.sh/ - all the code is inline in the page, so open the console and drop in breakpoints.

Step 3: Page Scaffolding

I simply created a basic page that loads the Bootstrap 3.x css from a CDN. Almost all our javascript is in-line in the page. If you are looking to optimize pageload, you could strip this down to ~30 lines of code, and stuff it in the <head> if you wanted to initiate fetching a webmap or some other resource as early as possible (read: before any frameworks have had time to load)

Step 4: Javascipt!

I put a lot of comments in the code, so here it is…

    // Setup some vars...
    // Client Id from your Application Registration at developers.arcgis.com
    let clientId = 'MReeG1Zt9JCulHLM';
    // change this if you are working with porta/enterprise
    let portalBaseUrl = 'https://www.arcgis.com';
    // does not matter what this is but it's used twice, so it's in a var
    let localStorageKey = 'agoauth';
    // simple "session" object
    let session = {
      isAuthenticated:false,
      portalInfo: null
    };
    let authObj = null;

    // With that setup, when the page loads, it will check for
    // a persisted session and it will automatically log the user in

    // check if the browser supports localStorage...
    if (window.localStorage) {
      let authObjItem = window.localStorage.getItem(localStorageKey);
      if (authObjItem) {
        // localStorage stores keys with STRING values!
        authObj = JSON.parse(authObjItem);
      }
    } else {
      // use cookies - you're on your own for this :)
      // authObj = someCookieParsetFunction()
    }
    // if we got an
    if (authObj)
      // check if the token has expired
      let nowTs = new Date().getTime();
      if (authObj.validUntil > nowTs) {
        // token should be valid...
        this.validateToken(authObj)
        .then((response) => {
          session.isAuthenticated = true;
          session.portalInfo = response;
          updateUI();
        })
        .catch((err) => {
          alert(`ZOMG! It did a bad! (an error occured)`);
        })
      }
    } else {
      // no worries, we assumed the user was not authenicated
    }


    // To valudate a token we make a call to the portals/self end-point.
    // If this is successful it will return a big fat object with information
    // about the org's portal and the current user.
    function validateToken (authObj) {
      // We have pulled in fetch via polyfill.io, so this should work in oldish browsers
      return this.fetch(`${portalBaseUrl}/sharing/rest/portals/self?f=json&token=${authObj.access_token}`)
      .then((response) => {
        // fetch does not auto-parse into json... so...
        return response.json();
      })
      .then((response) => {
        // Esri API's rarely use http error codes, but they may return an error payload
        if (!response.error) {
          // store the authObj in local storage
          window.localStorage.setItem(localStorageKey, JSON.stringify(authObj));
          return response;
        } else {
          throw new Error(`Error in response: ${JSON.stringify(response)}`);
        }
      })
    }

    // redirect.html simply calls this function, with the url
    // from there, we parse out the hash into a set of key/values
    // and stuff that into an object that our session then stores
    function checkOAuthResponse (href) {
      // url will look like: "http://localhost:8080/redirect.html#access_token=THE-TOKEN&expires_in=7200&username=dbouwman
      // parse the href - this could be tighter, esp if you have something like lodash loaded...
      let hash = href.split('#')[1];
      let parts = hash.split('&');
      let authObj = parts.reduce((acc, part) => {
        let k = part.split('=')[0];
        let v = part.split('=')[1];
        acc[k] = v;
        return acc;
      }, {})
      // the response has an expires_in value that is seconds-from-now...
      // lets turn that into a real date, so we can check if the token is valid later w/o doing an xhr
      authObj.validUntil = new Date().getTime() + authObj.expires_in;
      validateToken (authObj)
        .then((response) => {
          session.isAuthenticated = true;
          session.portalInfo = response;
          updateUI();
        })
        .catch((err) => {
          console.error(`Error: ${err}`);
          alert('error authenticating! Check the console!');
        });
    }

    // Start the sign-in process by creating the authorize url...
    // To make this code easier to deploy, we construct the currentBaseUrl
    // using the browser's current location, and use that to construct the redirect_uri
    // Remember - your application registration at ArcGIS Online MUST include the
    // redirect_uri that you specify here, or it will barf at you!
    function startSignIn () {
      let currentBaseUrl = [window.location.protocol, '//', window.location.host].join('');
      let authorizeUrl = `${portalBaseUrl}/sharing/oauth2/authorize?client_id=${clientId}&response_type=token&redirect_uri=${currentBaseUrl}/redirect.html`;
      window.open(authorizeUrl, 'authWindow', 'menubar=no,location=yes,resizable=no,scrollbars=no,status=no,width=500,height=550');
    }

    // Signing out is simple - just null out the session properties and kill the item
    // from localStorage
    function signOut () {
      window.localStorage.removeItem(localStorageKey);
      session.isAuthenticated = false;
      session.portalInfo = null;
      updateUI();
    }

    // Normally, you'd use a framework of some sort for UI updates etc...
    // But we're keep'in it real (simple) here today...
    function updateUI () {
      if (session.isAuthenticated) {
        document.getElementById('signInBlock').setAttribute('style','display:none');
        document.getElementById('signOutBlock').setAttribute('style','display:block');
        // ES6 templates ftw! But only on very modern browsers!
        let userInfo = `
          <h3>Hello ${session.portalInfo.user.fullName}</h3>
          <p>You are part of the ${session.portalInfo.name} organization</p>
        `;
        document.getElementById('user-info').innerHTML = userInfo;
      } else {
        document.getElementById('signInBlock').setAttribute('style','display:block');
        document.getElementById('signOutBlock').setAttribute('style','display:none');
        document.getElementById('user-info').innerHTML = '';
      }
    }

    // attach event handlers
    document.getElementById('signInBtn').addEventListener('click', startSignIn);
    document.getElementById('signOutBtn').addEventListener('click', signOut);

Redirect.html

This file is super simple - it literally get’s a reference to the window that opened it, and calls the checkOAuthResponse function, then closes itself.

<!DOCTYPE html>
<html>
  <head></head>
  <body>
    <script type="text/javascript">
      function closeWindow () {
        var win=window.open('', '_top', '', 'true');
        win.opener = true;
        win.close();
      }
      if (window.opener && window.opener.parent && window.opener.parent.checkOAuthResponse){
        window.opener.parent.checkOAuthResponse(window.location.href);
        closeWindow();
      } else if (window.parent && window.parent.checkOAuthResponse) {
        window.parent.checkOAuthResponse(window.location.href);
      }
    </script>
  </body>
</html>

And that is it!

If you cloned the repo, you can run npm install which will install two packages

  • http-server, which is a simple command-line http server - great for very simple projects like this
  • surge, which allows you to host static websites, for free, with a single command. Sweeet.