Migrating Libravatar to the Persona Observer API

Libravatar recently upgraded its support for the Persona authentication system (formerly BrowserID).

Here are some notes on what was involved in migrating to the Observer API for those who want to do the same on their sites.

Moving away from hidden forms

Libravatar used to POST the user's assertion to the server-side verification code through a hidden HTML form, just like the example Python CGI from the BrowserID cookbook.

This was a reasonable solution when the Persona code was only needed on a handful of pages, but the new API recommends loading the code on all pages where users can be logged in. Therefore, instead of copying this hidden form into the base template and including it on every page, I decided to switch to a jQuery.post()-based solution prior to making any other changes.

As a side-effect of interacting with the backend in an AJAX call, the error pages were converted to JSON structures and are now displayed in a popup alert.

From .get() to .watch() and .request()

By far the biggest change that the new API requires is the move from navigator.id.get() to navigator.id.watch() and navigator.id.requets(). Instead of asking for an assertion to verify, two callbacks are registered through watch() and identification is triggered through request() (which triggers the onlogin callback).

In the case of Libravatar, this involved:

  • including the Persona Javascript shim on every page
  • moving the assertion verification code from the get() callback to the new onlogin callback
  • adding a redirection to the existing logout page from the new onlogout callback
  • sharing part of the session state (i.e. which user is currently logged in, if any) with Persona through the loggedInEmail option to watch()

One thing to note is that while loggedInEmail is going to be renamed to loggedInUser, this change hasn't hit the production version of Persona yet and so I reverted to the old name after noticing that onlogin was unnecessarily called on every page load (a fairly expensive operation given the need to transmit and verify the assertion server-side).

Simplifying Content Security Policy headers

The CSP headers that Libravatar used to set on the pages that made use of the Persona Javascript shim now need to be set on every page, which is actually a nice simplication of our Apache config.

Note that if your CSP headers still refer to browserid.org, you must change them to login.persona.org.

Letting Persona know about changes in login state

One important change with respect to the old API is that Persona now keeps track of the login state for your site. If Persona finds a discrepancy between its idea of what your state should be and what you are advertising, it will trigger the appropriate callback (onlogin or onlogout) and attempt to resolve the conflict.

This is a very important feature since it will enable features like global logout and persistent cross-device logins, but it does mean that you have to notify Persona whenever your login state changes. If you forget to do this, your state will be automatically changed to match what Persona expects to see.

In Libravatar, this means that when users delete their account, we need to kill their session and tell Persona about it (through navigator.id.logout()). Otherwise, Persona will log them in again, which will of course cause a new account to be provisioned.

Working around the internal login state

The most complicated part of this migration to the new API was around our "add email" functionality, which lets users add extra emails to their existing Libravatar account.

With the old get() API, adding emails was as easy as requesting additional assertions and verifying them. Under the Observer API, requesting an assertion also changes the internal state that Persona keeps for that website. In practice, it means that after adding a new email in Libravatar, we need to update the "logged in" identifier to match the new one. Failure to do this will prompt Persona to invoke the onlogout callback with a different email, which will cause the email to get added to a new Libravatar account instead.

There are also two corner cases where Libravatar needs to fallback to its manual authentication backend and tell Persona that nobody is logged in:

In any case, despite these hacks, I got it all working in the end which is why I'm hopeful that we'll find a way to support this use case.

Taking advantage of the new features

The most visible feature that the new API brings (as options to request()) is the ability to add your name and logo to the Persona popup window:

The second feature I tried to enable on Libravatar is the new redirectTo request() option. Unfortunately, I had to revert this change since in our case, going straight to the profile page causes the @login_required Django decorator to run before the onlogin callback has a chance to set the session cookie.

In any case, redirecting to the login page already worked and so Libravatar probably doesn't need to make use of this Persona feature.

Conclusion

This migration was harder than I was expecting, but I'm confident that it will become easier in the next few weeks as the implementation is polished and documentation refreshed. I'm very excited about the Observer API because of the new security features and native integration it will enable.

If you use Persona on your site, make sure you sign up to the new service announcement list.