Biscuit, the foundation for your authorization systems
After 2 years of development, I am proud to share with you the official release of Biscuit, the authentication and authorization token we develop to manage access to our systems.
Where does it fit in the current authentication projects landscape (and why all of those cake themed names)?
- Cookies are a storage area in browsers, which can contain a session identifier
(the session data is then in a database, indexed by those identifiers), or
authentication tokens. They're good with lots of chocolate chips.
- JSON Web Tokens or JWT
(pronounced "jot") contain
cryptographically signed data. Since the signature guarantees it has not been
modified, a web application could store session data in a JWT and send it in
a cookie, and read it from HTTP requests. The signature can be done with
secret key cryptography (HMAC algorithm), or public key cryptography (RSA,
ECDSA). They can even be encrypted, and stored in a cookie, but they cannot be
- Macaroons are cryptographically
signed (HMAC) tokens focused on authorization. They embed caveats, conditions
that the request must fit. They support attenuation: the holder of a token can
create a new valid token by adding a caveat, further restricting the token. A
macaroon can be stored in a cookie. It is also an Italian almond or coconut-based cake (do not confuse it with the French macaron which is also an almond
- Open Policy Agent is a server-side logic
language used to encode authorization policies
Biscuit unifies these various approaches:
- it can be signed with public key or secret key cryptography like JWT
- it can be attenuated like Macaroons
- it comes with a powerful logic language to write authorization policies, like OPA, but those policies can also be carried by the token
By assembling those techniques, it opens up an array of authorization patterns that were not possible before.
When we started working on Biscuit, we were battling common issues in modern web applications:
- In a microservices system, how do you handle authorization from an initial
request, as it goes from service to service?
- How do you reconcile an application's authorization policies (often some basic
roles and groups) with a client's organization chart?
The microservices case is tricky: the initial request may come from a user for which we can look up a list of rights, but some services in the request tree may not even have a concept of user: at Clever Cloud, the service that launches virtual machines never hears about who requested a new deployment. With JWT, you could generate a temporary token in the user-facing API, and carry that from service to service. But then, any service holding that token has the entire set of rights for that request. Also, we need to make sure the authorization policies are evaluated in the same way in all services. With Macaroons, a service can attenuate the token before sending it to the next service, by adding a caveat, a condition over the current request (expiration date, limiting to read operations, restricting file paths to a prefix…). Unfortunately, Macaroon validation requires knowing the secret key used to generate the initial token.
Macaroons use a design based on chaining HMAC calculations: start from the initial secret, sign the first caveat, then for each new caveat, sign it using the previous signature as key. If you know the initial secret key, you can reconstruct the entire chain and verify that you obtain the same initial signature. But distributing that key in every service is a security risk: if someone gets access to this key, they can create a token with any authorization level they want. On the other side, JWT only requires verifiers to know a public key, and the private key can be kept in the service creating the token.
That was one of the motivating goals for Biscuit: what if we could attenuate the token, but still be able to verify it with public key cryptography?
As it turns out, a cryptographic concept called aggregated signatures can help us: we take multiple messages, each individually signed with a different public key, and we aggregate all of those signatures into one main signature. From that aggregated signature, it is impossible to remove one of the messages and keep a valid signature, but we can always add more signed messages. We can verify the aggregated signatures if we know the public keys for each message. From this, we reproduce the Macaroon design, with public key cryptography.
To provide attenuation, we could have reused the Macaroons approach with caveats, but its user experience was challenging: a caveat is basically a byte array for which you must design your own system to encode and test conditions.
For Biscuit, we chose a more general approach. We provide a logic language based on Datalog to write authorization policies. It can store data, like JWT, or small conditions like Macaroons, but it is also able to represent more complex rules like role-based access control, delegation, hierarchies. Those authorization policies can be carried by the token or provided on the verification side. They are encoded in a small binary format for transport. Additionally, it is fast to evaluate: generally, the entire process of checking the signature, deserializing the token and testing the authorization policies is done under 1 ms.
With this language (that can be learned in minutes), you get a unified way of representing complex business rules, in a testable and portable format. You can explore how policies work in a simulate environment, even write unit tests for them, then deploy them as dry-run tests and see how they would react on real world requests. Instead of a binary allow/deny result, you can gain fine-grained info, and query structured data. As an example, a request to list files would be accepted if we have the rights for it, and we can also get the filtered list of files we can access, even taking into account the attenuation rules carried by the token.
Multiple rule systems can be combined, which is useful for the second problem, about the mismatch between an application's policies and its user's needs:
- an application using GitHub or Twitter OAuth and requesting too many rights
because to get a subset of rights like read access to a repository, you get
it for all repositories
- a SaaS application or hosting company for which all users from one client
share one account
- or roles and groups that do not match work segmentation for users
Traditionally, this is solved in two ways:
- the service includes more and more complicated authorization policies and the
user management panel becomes a complicated mess
- it connects itself to external authorization systems, like Active Directory
or Keycloak, and let the user manage them
With Biscuit, there's another way. Authorization policies can be provided by the verification service, but they can also be carried by the token. The service can specify its policies, and the user can attenuate tokens with their own policies. And they will all be evaluated in the same way, while guaranteeing that the token cannot get more rights with user policies. So from an initial token, an entire parallel authorization design can be developed that will still be compatible with the original one.
You can also take an existing token, and restrict its access to a minimal set of resources, like you would need for your CI/CD systems. There's a lot of new patterns that will become possible with Biscuit, and we'll have to explore it more in the future. Right now, let's look at an existing use case.
At Clever Cloud, we are heavy users of Apache Pulsar. To provide this service to our users, we needed a flexible way to make it multitenant. By integrating Biscuit as an authorization plugin, using the Java implementation of Biscuit, we can provide a separate namespace for each user, but that token has full rights on that namespace. From there, the token can be attenuated to new tokens with various policies:
- limiting access to a topic name prefix
- allowing subscription on only one topic
- allowing message production on only one topic
- adding an expiration time
The authorization plugin only needs to check the token's initial rights to the namespace, and verify that the request matches the various checks added in attenuation.
As an example, we use that internally for a remote administration agent. Each new instance of the agent gets a new token derived from the original one, restricted to listening on its own topic (the topic name is a UUID). Then, when it receives a message, it also gets a short-lived token that can be used to send answers to a single temporary topic.
The next article will dive into how to write policies and how to integrate it into your application. You can already test that language in the online playground.
The specification is developed in the open, you can contribute.
We are just at the beginning of this exciting new technology, so we are still learning how to use it, exploring new design and authorization patterns. I can't wait to see the fun applications you will come up with Biscuit!