Adventures in client-side routing with re‑frame and OAuth

It’s been a while since my last post; one of the few disadvantages of working at a company you like a lot* is that as your work projects get more interesting, the urge to hack on external stuff in the interim diminishes. But having had some downtime and considerable anxiety to burn off in the last week or two, I revived one of my old open-source ideas: Haunting Refrain, a little single-page app that gets your Foursquare check-in history and then builds a random Spotify playlist out of random data from the places you’ve been to.

This is still very much a work in progress, but I’m pretty pleased with its direction and it uses a lot of intriguing tech. It’s all written in ClojureScript, and it uses re-frame for the basic single-page app control flow and UI bits. re-frame was written by Mike Thompson, probably Clojure’s greatest living essayist, and is an excellent package somewhat in the elm / redux / FRP-if-you-squint vein. I’m also now using datascript to retain more domain focused data and posh to wire it up to the UI elements, about which more later.

In a previous post, well over a year ago now, I talked about some difficulties I was having with dealing with external OAuth-style authentication in single-page apps. Having obtained a great deal more experience with client-side routing since then, I managed to solve this fairly quickly in Haunting Refrain (if you don’t count the time I spent bashing my head against similar problems while at work as trying to figure it out, I guess).

My current set-up uses pushy to handle the HTML5 history setup and sibiro as a routing library, largely because it’s the only client-side routing library whose README I can read without the risk of developing a migraine (honestly, client-side routing should not be very complex, what’s the deal?). I define a big route table which uses keywords for every route; each entry has a URL-matching pattern and a reference to a reagent component which will be used to render the given page.

On the re-frame side I keep a value in the app-db called :route/current-page which keeps track of the keyword matching the current URL. Whenever the URL has been changed, whether by the user landing on the page for the first time or from a link being clicked or the app itself redirecting the user, pushy will dispatch a new re-frame event of the form [:route/changed route-keyword route-params-if-any]. The handler for that route just persists those two values in the app-db, and then in the view code there’s a subscription which pulls out the two values, checks the big route table for the component matching the current route keyword, and renders the component, passing it the page parameters in case it needs them.

So that bit is pretty straightforward, and I’m pleased with how declarative the routing table winds up. For the spotify and foursquare OAuth callback pages, I’m cheating a little bit. When the user needs to authenticate, he or she will be redirected to Foursquare, will hit the “allow access” button, and will then be redirected back to a specific callback URL on Haunting Refrain. Back on the site, pushy parses the callback URL as :foursquare/hello, and the component which is associated with that route renders a blank page and dispatches an event when it is mounted into the DOM. The handler for this event parses the OAuth access token out of the URL, saves it in the app-db, persists it into HTML5 LocalStorage, and then redirects the user back to the home page at :main/index.

The LocalStorage bit is an interesting side track. Since the user winds up seeing a full page refresh whenever he or she is redirected for authorization, the application essentially has three entry points where it needs to construct the user’s state from scratch: on the home page, and then on the authorization callback pages for both Spotify and Foursquare. Since, in theory, we need to store access token for both services, the volatile re-frame app-db is not going to cut it. In an earlier version of the application, you could authenticate to Foursquare, get an access token, then authenticate to Spotify, at which point a full page refresh wiped out the Foursquare token.

Persisting stuff to LocalStorage wound up being pretty easy, and relies on the new re-frame effect and coeffect system. I have a re-frame effect :persist! which, when returned from an event handler, will write a value to the browser via the hodgepodge library. A matching :local-storage coeffect pulls it out of the browser, and I’m using this during app initialization to seed the database with any previously-retrieved access tokens. This works great, and should make it quite easy to persist arbitrary data across application invocations.

Overall I’m quite happy with re-frame. I’ve found that it can be a little difficult to track the control flow once a system gets reasonably complex, since you’re essentially encoding most of it by means of constructing a large bespoke FSM, but it does a fantastic job of keeping the control flow and display logic separate, and it is fairly easy to tinker with.

I’ll have more to say about Haunting Refrain in the days to come—the routing stuff isn’t actually one of the more interesting parts of it, the datascript and posh bits are. The thing is still buggy as heck and not deployed anywhere, but it’s runnable locally. Check it out!


* Workframe – we’re hiring!

Authentication, state and single-page apps in ClojureScript

I’ve been spending a lot of time recently working on a single-page app in ClojureScript, most recently using the recently-released, and impressive, re-frame project, which builds an FRPish unidirectional data flow on top of the ClojureScript react interface reagent. By a single page app, I mean one with almost no server-side code, which could theoretically be served from a static HTML page, and which only lives at a single URL (modifying the #fragment part of the URL to navigate between logical “pages” in the app, as is the vogue these days).

I’ve tried out a few different approaches to writing this app, including with Om and plain reagent, and re-frame seems to have just the right combination of simplicity and abstraction for my taste. With reagent I sometimes get the feeling I’ve had in backbone (JavaScript) projects, that there’s not really very much structure in the framework and I have to make a lot of stuff up on my own, which is fine but raises the question of why I’m using a framework at all. Om seems pretty neat, and definitely has a structure to it, but I’m not crazy about the ways it relies on async channels to communicate things between components. (In fairness, I’ve probably spent the least amount of time with Om, partly because the tutorials for it seem a little esoteric.)

At any rate, the app as it currently exists is pretty simple. There is a home page, from which the user is prompted to log into an OAuth service (in my case, foursquare). When the user hits the link, he or she will be redirected to foursquare to authorize my app, after which he or she will arrive back at my site with an authorization token in the URL (this is the “callback URL”, cf foursquare). I need the token in order to pass it along to foursquare for API requests I subsequently make to get the user’s check-in history and so forth.

With a full-stack application this would be pretty easy – my server-side code could look for the callback URL, and when it’s found it could grab the token, redirect the user to a known “thanks for logging in” page, and pass the token along to the user’s browser in a cookie, or embedded on the page, or though any number of other well-known server/browser mechanisms.

With a purely client-side app, however, the situation is trickier. In general, the user is expected to stay on a single web page the entire time he or she is using the app, without refreshing; application state sort of accumulates over time in the DOM and JavaScript object models on the page. But since the user needs to redirect to foursquare’s site in order to authenticate, my application now effectively has two entry-points: one when the user first navigates there, and one when the user returns from authentication. Indeed, since in the long term I want my app to authenticate against at least one more OAuth service, and probably several (spotify, twitter, etc), it will have an arbitrary number of entry points.

This complicates managing user state, since the user’s previous state (as reflected in the page’s object model) will be completely destroyed when he or she leaves the site for authentication. Upon the user’s return, the sum total of his or her state will essentially consist of the callback URL, including the authentication token. This is somewhat manageable with a single OAuth provider; when the user hits the callback URL, our client-side code can stash the token in the page’s object model somewhere and then use something like history.replaceState() to modify the URL back to the landing page. With more than one OAuth provider this approach is problematic, since the state will disappear when the user hits the second provider.

So we need a way to persist information between page refreshes. The two most obvious methods are cookies and HTML5 localStorage, and I will be using localStorage (or sessionStorage) because it is new and shiny. With that said, there are still two viable approaches I can see to this.

  1. User chooses to authenticate to foursquare, is redirected to foursquare, and then arrives back at the site with an empty app state and an auth token in the URL. The client-side code stashes the auth state in localStorage and uses replaceState() to navigate back to the home page.
  2. When the user hits the “log in” button, we open a popup. In the popup, the user is redirected to foursquare. When the user comes back to the popup page after authentication, the popup sets the token value in localStorage and closes itself. When localStorage is set, this will trigger a "storage" event in the parent window, which can then react by updating its state (to say “thanks for logging in” or the like).

There are a lot of things I like about the second approach. Because the original page persists while the popup is open, it can just trundle along as it had before, waiting for the storage event to fire; this simplifies state management in the parent window. However, it has a two considerable drawbacks:

Firstly, desktop browsers tend to block popups invoked from javascript these days, and honestly, thank god they do. There is probably a way to work around this, maybe by using an <a target="_blank"> tag or the like, but most of my experiments so far have triggered the popup blocker in my browser.

Secondly, popups kind of suck in mobile browsers. They work, more or less, but they don’t feel native to the mobile experience.

In passing, I’ll note that using an iframe might also be a technical solution to the two above problems, but I don’t really want to because (a) the user should see the address bar in an OAuth situation to validate that they’re not on a phishing site, and (b) ick, iframes.

So it seems like a straightforward redirect is the way to go. In my next installment, I’ll dig a little deeper into what this means for state management in the app, how that works with re-frame, and into client-side routing in ClojureScript generally.