Ampersand & OAuth2, Part 1: Local Storage

We’re going to try something new today. We’re going to remember I’m a developer, and I’m going to post some code on my blog, and we’ll all just see how that works out, ok? To kick it off, I’m gonna do a small series on using Ampersand.js as the front end for an API that uses OAuth2 for the authentication. In this post, we’re going to cover storing, loading, and maintaining the tokens and their metadata.

So let’s get to it.

Our Setup

I’m going to assume that you’re using ampersand-app and ampersand-model. If you’re not, just move along, this article will very likely be useless to you. I’m also going to assume you’re using an ES6 transpiler, like Babel. Because, well, I am. And it’s nice. So we’re going to, just this once, have nice things.

Our problem (today) is, once we get the user authenticated, we need to keep track of their OAuth access token and refresh token. If we lose them, the user has to re-authenticate, and that’s not a fun user experience. So we want to avoid that.

Fortunately, someone smarter than me has an example for how to do that using ampersand-model. So we’re going to copy Henrik’s “Me” model:

import Model from 'ampersand-model'

export default Model.extend({
  url: 'https://auth.server/path/to/token/endpoint',
  props: {
    access_token: 'string',
    refresh_token: 'string',
    expires_in: 'int',
    token_created: 'date',
    profileID: 'string',
  },
})

So far, so good. We’ve got a basic model with some properties. Nothing earth-shattering here.

We’re going to skip how to go ahead and populate the data. That’s, frankly, boring. (Hint: use ampersand-sync if you want to get some cool features from the next part of this series.)

For now, we’re just going to assume you get a populated Me instance, somehow.

Storing the Tokens

We want to be able to cache that information in localStorage. The helper to do that is tiny:

writeToCache () {
  const data = JSON.stringify(this)
  localStorage.setItem('me', data)
}

Super silly simple, right? Let’s put it all together to just highlight how silly it is that I’m writing a whole blog post about this:

import Model from 'ampersand-model'

export default Model.extend({
  url: 'https://auth.server/path/to/token/endpoint',
  props: {
    access_token: 'string',
    refresh_token: 'string',
    expires_in: 'int',
    token_created: 'date',
    profileID: 'string',
  },
  writeToCache () {
    const data = JSON.stringify(this)
    localStorage.setItem('me', data)
  },
})

You’ll notice that we’re using JSON.stringify(this) instead of this.toJSON(). If you check the docs, you’ll see that this.toJSON() does not, in fact, do what you think it does (emphasis mine):

Return a shallow copy of the state’s attributes for JSON stringification. This can be used for persistence, serialization, or augmentation, before being sent to the server. The name of this method is a bit confusing, as it doesn’t actually return a JSON string — but I’m afraid that it’s the way that the JavaScript API for JSON.stringify works.

I draw attention to this because it bit me. The moral of the story is that this.toJSON() is a lying liar that lies, and there’s not much the very nice Ampersand folks can do about it.

Loading the Tokens

Continuing on our journey, however, it occurs to me that if you’re going to store your data, you should probably, at some point, load it. After all, if data is stored in a forest, and no, uh, trees are there to, err, load… you know what, never mind. Point is, saving data and not loading it is rather silly. So let’s write a helper!

load () {
  const data = localStorage.getItem('me')
  if (data) {
    const loaded = JSON.parse(data)
    this.set(loaded)
  }
  return this
}

Super exciting, right? Pull the string out of localStorage, and if it worked, parse the JSON string into a Plain Ol’ JavaScript Object. Once that’s done, use it to update the Me model with set.

So to keep our running sample all together:

import Model from 'ampersand-model'

export default Model.extend({
  url: 'https://auth.server/path/to/token/endpoint',
  props: {
    access_token: 'string',
    refresh_token: 'string',
    expires_in: 'int',
    token_created: 'date',
    profileID: 'string',
  },
  writeToCache () {
    const data = JSON.stringify(this)
    localStorage.setItem('me', data)
  },
  load () {
    const data = localStorage.getItem('me')
    if (data) {
      const loaded = JSON.parse(data)
      this.set(loaded)
    }
    return this
  },
})

Set It & Forget It

We have a helper method to save our tokens to localStorage, but who wants to use that? You have to remember to call it every time your tokens change, they can get out of sync, it just feels like a pain.

I have a better idea.

One of the things we get for free from ampersand-model (thanks, ampersand-model!) is a set of events that get fired off. One of those events is the change event, which gets triggered when an attribute changes.

That sounds super handy. Let’s hook it up!

In your main.js, or whatever file is serving as the entrypoint for your app, include this:

import app from 'ampersand-app'
import Me from './me'

window.app = app.extend({
  init () {
    this.me = new Me()
    this.me.on('change', this.me.writeToCache)
  }
})

app.init()

That’s just your basic ampersand-app setup. We’re attaching an instance of your Me model to the app singleton, so we can refer to the same instance throughout our application. Then we’re listening for changes to our instance, and calling its writeToCache function when changes occur. This means whenever the tokens change, they automatically get persisted to localStorage for you. You don’t even have to think about it.

Go ahead, try it. I’ll wait.

But we have some issues. Some things (ampersand-sync comes to mind…) aren’t very conservative about updating things all at once. Sometimes they update properties one at a time. Which means we’d write to the cache every single time any property changed, instead of trying to batch them. That’s no good. We should obviously be batching these changes to reduce our resource usage.

The trick is to debounce the writeToCache function. That just means that the function will only be called once every so often, rolling multiple calls in that time period into a single call. Which sounds like what we want.

Here are the modifications you need:

import Model from 'ampersand-model'
import debounce from 'lodash.debounce'

export default Model.extend({
  url: 'https://auth.server/path/to/token/endpoint',
  props: {
    access_token: 'string',
    refresh_token: 'string',
    expires_in: 'int',
    token_created: 'date',
    profileID: 'string',
  },
  initialize () {
    this.debouncedWriteToCache = debounce(this.writeToCache, 250)
  },
  writeToCache () {
    const data = JSON.stringify(this)
    localStorage.setItem('me', data)
  },
  load () {
    const data = localStorage.getItem('me')
    if (data) {
      const loaded = JSON.parse(data)
      this.set(loaded)
    }
    return this
  },
})

All we did was add an initialize function to our Me model, and have it set this.debouncedWriteToCache to be a debounced version of writeToCache, so multiple calls within 250 milliseconds are turned into a single call. There’s no reason we should be modifying our tokens more than once every 250 milliseconds, anyways.

That done, we can update our application bootstrap to use the debounced version:

import app from 'ampersand-app'
import Me from './me'

window.app = app.extend({
  init () {
    this.me = new Me()
    this.me.on('change', this.me.debouncedWriteToCache)
  }
})

app.init()

Now no matter how many properties change, you only write to localStorage once. Hooray!

OK, so we have saving taken care of automatically and we don’t need to think about it anymore. How about loading?

That’s actually phenomenally simple. Ready for this big code change?

import app from 'ampersand-app'
import Me from './me'

window.app = app.extend({
  init () {
    this.me = new Me().load()
    this.me.on('change', this.me.debouncedWriteToCache)
  }
})

app.init()

Yeah, we added .load() to the end of our instantiation for this.me. Super exciting stuff. It just loads the tokens out of localStorage when your app starts up.

And you’re done! Now your tokens save automatically, get loaded automatically, and you can just treat them like they’re stored in memory all the time. No muss, no fuss.

Paddy, You Just Blogged About Like 40 Lines of Code

This blog post was dealing with an almost ridiculously trivial block of code. There’s nothing fancy in my code samples: I’m setting properties, parsing JSON, and using localStorage.

But the simplicity of the code is the point of this post. Ampersand’s event system let me use 40 lines of code to abstract away what could otherwise be a rather messy part of the application.

And if you stick around for the next post in the series, we’re going to leverage this into truly mind-bending levels of “don’t make me think”.

If you want, for some reason, a running version of this code (along with a transpiler to build it so it functions in your browser), I put it all in a Github repository. Because the only thing funnier than writing a big long blog post about 40 lines of code is putting it in a Github repo and slapping a license and README on it.