My First Macaroon: A New Way to do Authorization

A Macaroon

Macaroons are the hot new authorization credential developed by the folks at Google for use in their authorization systems. Like cookies, macaroons are bearer tokens that enable the token-holder to show that they are authorized to access a service. In this article, we'll walk through installing the macaroons library and minting your first macaroon.

Installing Macaroons

Before you can create your first macaroon, you'll need to install the macaroons library and its dependencies. If you're running a recent version of Ubuntu, Debian, CentOS, or Fedora, you can head on over to the HyperDex downloads page and follow the instructions to add the HyperDex repo. Instead of installing HyperDex, install python-macaroons. If you're running Ubuntu 14.04 (the latest release), you would do:

$ sudo su -
# wget -O - http://ubuntu.hyperdex.org/hyperdex.gpg.key | apt-key add -
# wget -O /etc/apt/sources.list.d/hyperdex.list http://ubuntu.hyperdex.org/trusty.list
# apt-get update
# apt-get install python-macaroons

If you're more the build-it-from-source kind of person, that's almost as easy. You'll need to install libmacaroons and its dependency, libsodium, and the from-source instructions cover both:

$ sudo apt-get install build-essential python-dev

$ wget https://github.com/jedisct1/libsodium/releases/download/0.5.0/libsodium-0.5.0.tar.gz
$ tar xzvf libsodium-0.5.0.tar.gz
$ cd libsodium-0.5.0
$ ./configure && make && make check && sudo make install

$ wget http://hyperdex.org/src/libmacaroons-0.1.0.tar.gz
$ tar xzvf libmacaroons-0.1.0.tar.gz
$ cd libmacaroons-0.1.0
$ ./configure --enable-python-bindings && make
$ sudo make install

You now have macaroons installed and are ready to go!

Creating Your First Macaroon

Another Macaroon

Mmmm... Tasty!

Imagine that you run a website, say a bank, and were looking to use macaroons for authorization. You can create a macroon that authorizes access to your website like this:

>>> import macaroons
>>> secret = 'this is our super secret key; only we should know it'
>>> location = 'http://mybank/'
>>> public = 'my bank macaroon'
>>> M = macaroons.create(location, secret, public)

We've created our first macaroon!

You can see here that it took three pieces of information to create a macaroon. The first one is a secret, which will be used to tell apart the people who know that secret, and therefore should have access to the website, from those who do not. Here, we just have English text, but in reality we would want to use something more random and less predictable.

The second piece is a location, which identifies the resource for which this macaroon is the key. In this case, this macaroon will authorize accesses to our bank, so it carries the (fake) URL for our bank. The location is purely a hint; it's a free-form string maintained to help applications figure out where to use macaroons -- the libmacaroons library (and by extension, the Python bindings) do not ascribe any meaning to this location.

The third piece is a public identifier, which is generally free-form and is there for your convenience. If you assign these from a unique pool, they can be used to tell apart macaroons from each other, to remind us which secret we used to construct the macaroon, or for anything else you like. Anyone in possession of the macaroon can see the public portion as well as the location hint:

>>> M.identifier
'my bank macaroon'
>>> M.location
'http://mybank/'

Each macaroon, by default, is assigned a signature that is used to add caveats and verify the macaroon. The signature is computed by the macaroons library, and is unique to each macaroon. Applications never need to directly work with the signature of the macaroon. The signature is publicly accessible:

>>> M.signature
'a7e108fa16baeffab01f44bad8b22a8de76e3fc593f3f85efd5b95b1584780c6'

We can share this macaroon with others by serializing it. The serialized form is pure-ASCII, and is safe for inclusion in secure email, a standard HTTPS cookie, or a URL. We can get the serialized form with:

>>> M.serialize()
'MDAxY2xvY2F0aW9uIGh0dHA6Ly9teWJhbmsvCjAwMjBpZGVudGlmaWVyIG15IGJhbmsgbWFjYXJvb24KMDAyZnNpZ25hdHVyZSCn4Qj6Frrv+rAfRLrYsiqN524/xZPz+F79W5WxWEeAxgo='
>>> M2 = macaroons.deserialize(M.serialize())
>>> M2.signature
'a7e108fa16baeffab01f44bad8b22a8de76e3fc593f3f85efd5b95b1584780c6'

Of course, macaroons can be displayed in a more human-readable form:

>>> print M.inspect()
location http://mybank/
identifier my bank macaroon
signature a7e108fa16baeffab01f44bad8b22a8de76e3fc593f3f85efd5b95b1584780c6

Adding Caveats

More Macaroons!

At this point, we have an unconstrained macaroon that authorizes everything within our bank. It's kind of like a master lock that allows the holder to do whatever they like. In practice, such a macaroon is dangerous, and what we will want to do is to mint more restricted macaroons that we can then hand out to people.

Let's add a caveat to our macaroon that restricts it to just the account number 3735928559.

>>> M = M.add_first_party_caveat('account = 3735928559')

This new macaroon includes the same identifier and location that our old macaroon from our initial macaroon, but includes the additional caveat that restricts the bank account. The signature of this new macaroon is different, and incorporates the new caveat we've just added. An entity in possession of this new macaroon cannot simply remove our new caveat to construct the old macaroon:

>>> print M.inspect()
location http://mybank/
identifier my bank macaroon
cid account = 3735928559
signature 365a7df06d625e30fe953061c5a82c12a64736335d2b5904f2356c6de7ba6a19

Of course, we can add a few more caveats, and the macaroon's signature will change with each of them.

>>> M = M.add_first_party_caveat('time < 2015-01-01T00:00')
>>> M.signature
'aaad3f76e0fefe3f55502af2a19dc22be9481879f3075dc0b626eccca45cc6d7'
>>> M = M.add_first_party_caveat('email = alice@example.org')
>>> M.signature
'2fe18c3363cfba001f2488cd5106fe9fd35aa75899ee4013a3681caacb2733b0'
>>> print M.inspect()
location http://mybank/
identifier my bank macaroon
cid account = 3735928559
cid time < 2015-01-01T00:00
cid email = alice@example.org
signature 2fe18c3363cfba001f2488cd5106fe9fd35aa75899ee4013a3681caacb2733b0

The combination of all caveats in this macaroon authorize alice@example.org to access account 3735928559 until the end of 2014. Alice may present this macaroon to the bank any time she wishes to prove to the bank that she is authorized to access her account. Ideally, she'll transmit the serialized form of the macaroon to the bank:

>>> msg = M.serialize()
>>> # send msg to the bank

Verifying Macaroons

Our bank application's purpose is to protect users accounts from unauthorized access. For that reason, it cannot just accept anything that looks like a macaroon---that would defeat the point of using macaroons in the first place. So how can we ensure that only authorized users access the bank?

Yet Another Macaroon

We can determine whether a request is authorized through a process called verification. First, we construct a verifier that can determine whether the caveats on macaroons are satisfied. We can then use our verifier to determine whether a given macaroon is authorized in the specific context of the request. For example, our bank account application knows the account number specified in the request, and can specify account = # when building the verifier. The verifier can then check that this matches the information within the macaroon, and authorize the macaroon if it does indeed match.

Let's walk through the verification process for Alice's macaroon that we constructed in the previous section. The first step, of course, is for the bank to deserialize the macaroon from the message. This converts the macaroon into a form we can work with.

>>> M = macaroons.deserialize(msg)
>>> print M.inspect()
location http://mybank/
identifier my bank macaroon
cid account = 3735928559
cid time < 2015-01-01T00:00
cid email = alice@example.org
signature 2fe18c3363cfba001f2488cd5106fe9fd35aa75899ee4013a3681caacb2733b0

We have the same macaroon that Alice believes authorizes her to access her own account, but we must verify this for ourselves. One (very flawed) way we could try to verify this macaroon would be to manually parse it and authorize the request if its caveats are true. But handling things this way completely sidesteps all the crypto-goodness that macaroons are built upon.

Another approach to verification would be to use libmacaroons's built-in verifier to process the macaroon. The verifier hides many of the details of the verification process, and provides a natural way to work with many kinds of caveats. The verifier itself is constructed once, and may be re-used to verify multiple macaroons.

>>> V = macaroons.Verifier()
>>> V # doctest: +ELLIPSIS
<macaroons.Verifier object at ...>

Let's go ahead and try to verify the macaroon to see if the request is authorized. To verify the request, we need to provide the verifier with Alice's macaroon, and the secret that was used to construct it. In a real application, we would retrieve the secret using M.identifier; here, we know the secret and provide it directly. A verifier can only ever successfully verify the macaroon when provided with the macaroon and its corresponding secret---no secret, no authorization.

Intuitively, our verifier should say that this macaroon is unauthorized because our verifier cannot prove that any of the caveats we've added are satisfied. We can see that it fails just as we would expect:

>>> V.verify(M, secret)
False

We can inform the verifier of the caveats used by our application using two different techniques. The first technique is to directly provide the verifier with the caveats that match the context of the request. For example, every account-level action in a typical banking application is performed by a specific user and targets a specific account. This information is fixed for each request, and is known at the time of the request. We can tell the verifier directly about these caveats like so:

>>> V.satisfy_exact('account = 3735928559')
>>> V.satisfy_exact('email = alice@example.org')

Caveats like these are called "exact caveats" because there is exactly one way to satisfy them. Either the account number is 3735928559, or it isn't. At verification time, the verifier will check each caveat in the macaroon against the list of satisfied caveats provided to satisfy_exact. When it finds a match, it knows that the caveat holds and it can move onto the next caveat in the macaroon.

Generally, you will specify multiple statements as exact caveats, and let the verifier decide which are relevant to each macaroon at verification time. If you provide all exact caveats known to your application to the verifier, it becomes trivial to change policy decisions about authorization. The server performing authorization can treat the verifier as a black-box and does not need to change when changing the authorization policy. The actual policy is enforced when macaroons are minted and when caveats are embedded. In our banking example, we could provide some additional satisfied caveats to the verifier, to describe some (or all) of the properties that are known about the current request. In this manner, the verifier can be made more general, and be "future-proofed", so that it will still function correctly even if somehow the authorization policy for Alice changes; for example, by adding the three following facts, the verifier will continue to work even if Alice decides to self-attenuate her macaroons to be only usable from her IP address and browser:

>>> V.satisfy_exact('IP = 127.0.0.1')
>>> V.satisfy_exact('browser = Chrome')
>>> V.satisfy_exact('action = deposit')

Although it's always possible to satisfy a caveat within a macaroon by providing it directly to the verifier, doing so can be quite tedious. Consider the caveat on access time embedded within Alice's macaroon. While an authorization routine could provide the exact caveat time < 2015-01-01T00:00, doing so would require inspecting the macaroon before building the verifier. Inspecting a macaroon's structure to build a verifier for it is considered bad practice.

So how can we tell our verifier that the caveat on access time is satisfied? We could provide many exact caveats of the form time < YYYY-mm-ddTHH:MM, but this would be inefficient. The second technique for satisfying caveats provides a more general solution.

Called "general caveats", the second technique for informing the verifier that a caveat is satisfied allows for expressive caveats. Whereas exact caveats are checked by simple byte-wise equality, general caveats are checked using an application-provided callback that executes when the macaroon is invoked and returns true if the caveat is true at that instant. There's no limit on the contents of a general caveat, so long as the callback understands how to determine whether it is satisfied.

We can verify the time caveat on Alice's macaroon by writing a function that checks the current time against the time specified by the caveat:

>>> import datetime
>>> def check_time(caveat):
...     if not caveat.startswith('time < '):
...         return False
...     try:
...         now = datetime.datetime.now()
...         when = datetime.datetime.strptime(caveat[7:], '%Y-%m-%dT%H:%M')
...         return now < when
...     except:
...         return False
...

This callback processes all caveats that begin with time <, and returns True if the specified time has not yet passed. We can see that our caveat does indeed return True when the caveat holds, and False otherwise:

>>> check_time('time < 2015-01-01T00:00')
True
>>> check_time('time < 2014-01-01T00:00')
False
>>> check_time('account = 3735928559')
False

We can provide the check_time function directly to the verifier, so that it may check time-based predicates.

>>> V.satisfy_general(check_time)

It's finally time to verify our macaroon! Now that we've informed the verifier of all the various caveats that our application could embed within a macaroon, we can expect that the verification step will succeed.

>>> V.verify(M, secret)
True

More importantly, the verifier will also work for macaroons we've not yet seen, like one that only permits Alice to deposit into her account:

>>> N = M.add_first_party_caveat('action = deposit')
>>> V.verify(N, secret)
True

More Macaroons; More Tasty Goodness

So far, this post has just scratched the surface of what macaroons enable us to do. If you're eager to learn more, here are some other links that might pique your interest.

  • The Macaroons Paper: The paper that describes everything about what macaroons are, and how they enable decentralized authorization. The implementation we worked with in this article stays faithful to the concepts described in the paper.
  • Macaroons @ Mozilla: Ăšlfar Erlingsson, one of the authors of macaroons, recently gave a tech talk at Mozilla about Macroons.
  • libmacaroons is the first open source implementation of macaroons. Checkout the GitHub README for a tutorial that walks through even more features of macaroons.
  • Sarah's Patisserie: If all this talk of digital macaroons has you yearning for the tasty dessert, Sarah's Patisserie has some of the best macaroons in Ithaca.
Share on Linkedin
Share on Reddit
comments powered by Disqus