Using Portier with Python's asyncio
23 Jan 2017
Updates
The asyncio-portier library
15 Feb 2017
I’ve published the asyncio-portier Python library. Read the announcement post here.
Original Post
Mozilla Persona was an authentication solution that sought to preserve user privacy while making life easy for developers (as explained here). You may have noticed the past tense there — Persona has gone away. A new project, Portier, picks up where Persona left off, and I’m eager to see it succeed. I’ll do anything to avoid storing password hashes or dealing with OAuth. With that in mind, here is my small contribution: a guide to using Portier with Python’s asyncio.
The Big Idea
So far, the official guide to using Portier is… short. It tells you to inspect and reuse code from the demo implementation. So that’s what we’ll do! But first, let’s take a look at how this is supposed to work.
The guide says
The Portier Broker’s API is identical to OpenID Connect’s “Implicit Flow.”
From the developer’s perspective, that means
- A user submits their e-mail address via a
POST
to an endpoint on your server. - Your server
- Generates and stores a nonce for this request.
- Replies with a redirect to an authorization endpoint (as of today, that
means hardcoding Portier’s Broker URL:
https://broker.portier.io
).
- Your server will eventually receive a
POST
to one of its endpoints (/verify
in the demo implementation). At this point- If the data supplied are all valid (including checking against the nonce created in step 2.1.), then the user has been authenticated and your server can set a session cookie.
- If something is invalid, show the user an error.
This is all well and good… provided that you’re using Python and your server code is synchronous. I generally use Tornado with asyncio for my Python web framework needs, so some tweaks need to be made to get everything working together nicely.
If you want to use something other than Python, I can’t really help you. I did say Portier is new, didn’t I?
Enter asyncio
For some background, Python 3.4 added a way to write non-blocking single-threaded code, and Python 3.5 added some language keywords to make this feature easier to use. For the sake of brevity I’ll include code that works in Python 3.5 or later. Here is a blog post describing the changes in case you need to use an earlier version of Python.
For those of you using the same setup that I do (Tornado and asyncio), refer to this page for getting things up and running.
The login endpoint
This code does not need to be modified to work with asyncio. I’ll include what it should look like when using Tornado, though. Assuming that
REDIS
is a Redis connection object as from redis-pySETTINGS
is a dictionary containing your application’s settingsSETTINGS['WebsiteURL']
is the URL of your application (such ashttps://www.example.com
)SETTINGS['BrokerURL']
is the URL of the Portier Broker,https://broker.portier.io
The verify endpoint
This does need some modification to work. Assuming that you have defined an
exception class for your application called ApplicationError
get_verified_email
This function only needs two straightforward changes from the demo implementation:
and
discover_keys
This function needs three changes from the demo implementation. The first is simple again:
The second change is in the line with res = urlopen(''.join((broker,
'/.well-known/openid-configuration')))
. The problem is that urlopen
is
blocking, so you can’t just await
it. If you’re not using Tornado, I
recommend using the aiohttp library (refer to the
client example). If you are using Tornado, you can use the
AsyncHTTPClient class.
The third change is similar to the second: raw_jwks =
urlopen(discovery['jwks_uri']).read()
uses urlopen
again. Solve it the same
way:
Wrapping up
Down the line, there will be client-side Portier libraries (or, at least, other demo implementations) for various languages. Until then, you’ll need to do some of the heavy lifting yourself. I think it’s worth it, and I hope you will, too.
« Blog
Comments: View this post's comment section here.