Macaroons are like cookies on steroids. And if we think of this concept in the context of authorization for web applications, it is still valid. Cookies is the most used authorisation mechanism in web applications, they are bearer tokens that enable their holder to access a certain service.

The folks at Google have created the Macaroons, they are like Cookies but they have contextual caveats that make them great for decentralized authorization.

To use them, you’ll need to install the libmacaroons, which is out of the scope of this article, but it’s a really straight forward procedure.

Alright, tell me more…

The definition from Google is Macaroons: Cookies with Contextual Caveats for Decentralized Authorization in the Cloud

:wat:

Well, as the definition says, they are cookies but they can have restriction rules to reduce the access permissions.

The main problem with Cookies is that they are easy to steal and, once stolen, the holder of the cookie will have full access to the service that issued it. To help mitigating this problem, the macaroons add integrity checks through HMAC signatures.

Macaroons have 3 properties:

  • secret: This is a string used to sign the macaroon and it’s held by the service minting the macaroon
  • location: This is a free-form string, generally used as a hint to know where the macaroon should be used
  • identifier: This is a public identifier, a free-form string to be used at your convenience

The location and identifier fields are public and visible to everybody. The secret is not shown and should be known only to the service issuing the macaroon.

With this information, the macaroon is signed, then serialized and delivered to the user just like any regular cookie. When the user sends it back to your service, you can check the signature of the macaroon using your secret and determine whether it’s a macaroon you’ve issued.

So far, this behaves just as a regular cookie, allowing its holder to fully access your service, but what really makes macaroons better cookies is that you can add arbitrary restrictions to them, the so called caveats.

Let’s say you want to restrict the access to a certain username and within a certain timeframe, so you add two layers of first party caveats for this. Here’s an example in Ruby:

require 'macaroons'
require 'securerandom'

secret     = Settings::SECRET
identifier = "users"
location   = "http://my.protected.service.com"

macaroon = Macaroon.new(key: secret, identifier: identifier, location: location)

macaroon.add_first_party_caveat("username = k4nd4lf")
macaroon.add_first_party_caveat("timestamp = #{(Time.now + 10).to_i}")

puts macaroon.serialize

=> MDAyZGxvY2F0aW9uIGh0dHA6Ly9teS5wcm90ZWN0ZWQuc2VydmljZS5jb20KMDAxNWlkZW50aWZpZXIgdXNlcnMKMDAxYmNpZCB1c2VybmFtZSA9IGs0bmQ0bGYKMDAxZmNpZCB0aW1lc3RhbXAgPSAxNDY0NDUwNTIxCjAwMmZzaWduYXR1cmUgwZDzE8jjOOmUn7uWQCRFwsmwadjP5Z5ZpI7zeLOHGFoK

By adding caveats, you attenuate the macaroon or, better said, you attenuate the macaroon’s rights, so it’s now valid only to access your service for a certain username and within a certain time.

OK, now what?

Let’s say you receive the macaroon from the client, you need to verify it in order to decide whether you allow the request.

require 'macaroons'
require 'securerandom'

# Deserialize the macaroon

macaroon = Macaroon.from_binary(ENV[HTTP_AUTHENTICATION])

verifier = Macaroon::Verifier.new

verifier.satisfy_exact("username = k4nd4lf")
verifier.satisfy_exact("timestamp = 1464568512")

puts verifier.verify(
    macaroon: macaroon,
    key: Settings::SECRET #this is your secret key, the one you used to sign the macaroon in the first place
)

=> true

The verifier checks the macaroon’s integrity by verifying the signatures of its caveats.

Yeah, yeah, take me to the interesting stuff…

Caveats are not only useful for attenuation, but also for delegation. Suppose you don’t want to be in charge of recognizing users and, instead, want to delegate this to a third party service, for example Google, or even a different service within your distributed system.

For this delegation, you need to have some sort of agreement with this service where the service has shared a key with you. With this key, you add a third party caveat, telling the user to collect another macaroon from another service who can grant her identity. You are, with this, discharging the responsibility of user identification. For example:

...

shared_caveat_key = "13210d098f4b" #This is the key the third party service shared with you
id_service_url    = "http://provider.identity.com"

macaroon.add_third_party_caveat(shared_caveat_key, "identity_caveat", id_service_url)

With this restriction, you’re saying the user will need to collect another macaroon from a different service that can be verified, otherwise, the macaroon you issued won’t be valid. At this point you send this macaroon to the user, but it doesn’t grant any permission without the macaroon you’re discharging the user recognition responsibility to.

The identity provider service might add any other caveat to the discharge macaroon, for example, an IP constraint to limit the identity grant to only a specific IP address.

# Identity Provider Service

discharge = Macaroon.new(key: "13210d098f4b", identifier: "identity_macaroon", "http://provider.identity.com")

discharge.add_first_party_caveat("ip_address = 107.170.61.140")

Now, the user sends you both macaroons in the request so you can verify her permissions. For example:

# Example of a Cuba web application
require 'macaroons'

Cuba.define do
  on get, "/locations" do
    macaroon  = Macaroon.from_binary(req.env["HTTP_AUTHORIZATION"])  #The original macaroon we issued
    discharge = Macaroon.from_binary(req.env["HTTP_AUTHENTICATION"]) #The discharge macaroon provided by the IPS
    verifier  = Macaroon::Verifier.new

    protected_discharge = macaroon.prepare_for_request(discharge)

    verifier.satisfy_exact("username = k4nd4lf")
    verifier.satisfy_exact("timestamp = 1464568512")
    verifier.satisfy_exact("ip_address = 107.170.61.140")

    verified = verifier.verify(
        macaroon: macaroon,
        key: Settings::SECRET,
        discharge_macaroons: [protected_discharge]
    )

    unless verified
      res.status = 401
      res.write "Unauthorized"
      res.finish
    end

    res.status = 200
    res.write "Locations"
  end
end

The sample code above knows the macaroons comes in the request headers, so it deserializes them from there and sets up a verifier.

The verifier is loaded with the rules it has to verify and then verifies the macaroon with its discharge macaroons and the application makes a decision based on its result.

There is also a general technique for caveats verification that allows you to specify an application provided callback function which receives the caveat as a parameter and performs the check, returning true or false indicating the validity of the caveat. For example, in the example above, we could replace the IP address caveat check with:

...

def check_origin(caveat)
  return false unless caveat.to_s.start_with?("ip_address = ")

  ip = caveat.split("ip_address = ").last
  allowed_origins = get_allowed_origins() #This should be a method that retrieves allowed IP addresses from a DB

  allowed_origins.include?(ip)
end

verifier.satisfy_general(method(:check_origin))

...

Of course, this is a trivial example, but using this technique you can perform a complex rules checking.

Awesome, now I want to know more

There’s also the possibility for third party (discharge) macaroons to include futher delegations to other services, this will imply that all discharge macaroons need to be provided for verification in order to verify the root macaroon.

This is out of the scope of this article, but if you’re intersted, you can read more technical details at the official publication from Google.

It is also highly recommendable to watch this talk by Ulfar Erlingsson from the Google Security Team at Mozilla which explains the concepts clearly for humans.

Both of these has been used as sources for my initial research and for this post.

– Leonardo Mateo (@k4nd4lf)