In this tutorial, we’ll learn about Biscuit authorization and create, authorize, and attenuate the tokens. It’s often painful to write and check authorization codes on every platform. Biscuit tokens help us save time and effort when making secure microservice applications, so we don’t have to worry about managing authorization across different services. Biscuits let us make tokens offline and authorize them by sending them to the server in the form of cookies. Here is a diagram to illustrate how Biscuit can be utilized across platforms.
Long Tutorial Alert!
This tutorial is a part of 2 tutorial series.
- Part 1: You are already here. This tutorial covers the basics of Biscuits, creating a biscuit with a Command Line interface (CLI), and checking the authority of the token. We will also learn to attenuate the Biscuit tokens
- Part 2: Upcoming tutorial.
This covers how to use Biscuits with Golang
What are Biscuit Authorization Tokens?
Biscuit Authorization tokens are Bearer tokens that hold information about a user’s permissions (authorization) for a given application. That means one doesn’t have to create a separate authorization table for users. The essence of Biscuit tokens can be better understood in a microservices architecture with multiple applications across multiple servers. Biscuit, being a token, can be used in any microservice application with a public key without any language dependency. Furthermore, permissions in Biscuits can be extended for a new type of user with fewer rights than the current user in the hierarchy. This process is called attenuation. Attenuation is achieved by writing the authorization policies in Datalog, a declarative programming language. The following diagram illustrates a use-case scenario for a forum website using biscuits.
Finally, if you don’t want the biscuit token to be further attenuated, you can Seal the biscuit. Also, you can manually decommission a biscuit token using the Revocation id.
One can port Biscuit tokens to various applications and implement them in multiple programming languages. The only things needed for its implementation are Protobuf generator and Ed25519 signing. We’ll look at how the CLI is used to implement Biscuit authorization, and in Part 2 we’ll learn how to use Biscuits with Golang
Biscuit vs JWT vs Macaroons
Biscuit combines the goodness of both JWT and Macaroons to deliver the best authorization options for an application while providing the best security.
JWT | Macaroons | Biscuit | |
Store as cookie | Yes | Yes | Yes |
Security | HMAC, RSA, ECDSA | HMAC | Ed25519 |
Attenuation support | No | Yes | Yes |
Authorization policy | No | Yes | Yes |
Components in Biscuit Authorization
A few important terms to remember
Facts
Facts are data or truth. Existing facts cannot be changed but a new fact can be generated from existing ones using Rules (explained below).
For example, facts can be
user("1234");
roles(["admin", "moderator"]);
Rules
Rules can be used to generate new facts. They accept facts as parameters and combine them with other facts to generate new ones.
Example of Rules
rights($id, $roles) <- user($id), roles($roles);
This rule generates a new fact like rights("1234",[ "admin", "moderator"]);
Checks
Checks are used to validate facts against certain values.
For example
check if roles($roleList), $roleList.contains("moderator");
The above condition checks if the roles list contains “moderator“. These checks can be written both in Biscuit token blocks or can also be written in the server-side application while validating biscuits.
Blocks
A biscuit token is made up of blocks. Each block can contain facts, rules, and checks. There is one block by default which is called Authority Block. Other blocks are user generated.
Allow/Deny
Allow/Deny conditions come after Checks if any, and they allow or deny access to resources. They can be used only in authorizer applications and not with Biscuit tokens.
Example usage
allow if resource($id, $res) <- user($id), somecase($res);
The above code can be interpreted as, if both the conditions on the right side pass, only the code will allow access to the resource.
Namespace
Namespaces are used to avoid accidental collision while naming facts in a microservices architecture.
ShoppingCart:User("1234");
AdminUSer:User("1234");
Here both “User” are from different namespaces and do not collide when writing rules.
Trying the Command Line Interface (CLI)
The Command Line Interface (CLI) is the fastest way to try out the Biscuit Tokens. We will learn how to install the CLI and run commands to create, authorize, and attenuate Biscuit. For detailed commands, visit this page.
Installing the CLI
You can either download the source code or use the Cargo package manager to install the binary file. We will follow the latter and won’t focus on compiling from source. Cargo is the package manager for the Rust programming language, and it should already be on your computer for this tutorial. Then you can install Biscuit CLI using the following command
> cargo install biscuit-cli
Note: You might need to set the environment variable path for biscuit-cli located in <install path>/cargo/bin. On Ubuntu, you can set this by adding the path to the /etc/environment file and then running the following command to instantly make the terminal parse the changes in the file
> source /etc/environment
Generating Public-Private Key Pair
Once you install the CLI, you can create a basic public-private key pair for your application. The server stores the public key, and it can use it to verify any token signed by the private key
> biscuit keypair
Generating a new random keypair
Private key: 1e4a2a2453da6528a1b72ea1e7ff1b76d1e67d883a53a1671f3b5b382bc11d51
Public key: 568e1a3876a327444f18414e66714d5deb908ba0b667ca9587bf9a03df8af478
Note: Create new files named private-key and public-key. Save the above-obtained private and public keys to the files, respectively. Don’t lose the private key, or else you won’t be able to authenticate your keys in the future. Regenerating the private key will be the only option then
Creating Biscuit Token
Authorization files can contain hardcoded Datalog blocks or scripts. For the sake of this tutorial, we’ll stick to hardcoded Datalog blocks. If you want to learn Datalog scripting, please visit this link.
We will save the following content to the file as “authority.datalog.”
user("admin");
The block above describes the role of the user holding this biscuit token. These data are called “facts” and will be checked later against the rules on the server. The facts can be hardcoded data or can be derived using various rules.
The following type of content can be added to a biscuit token
- Facts: Hardcoded data like
user(["admin", "moderator"])
- Rules: To generate more facts based on conditions. Explained later.
- Checks: Add checks to limit the token. They are also used in attenuation which is described later.
Now, we will create a new Biscuit token with the above role and sign it using our private key. To create a Biscuit authorization token, use the following command
> biscuit generate --private-key-file private-key authority.datalog
// This is the generated Biscuit token
EnYKDBgDIggKBggKEgIYDRIkCAASIHTT7y36m_zlF2BqdHkkXbu8no8u8tXQI06_BPmW2v17GkCnqmi4dKgkZX5b45542A5Ksuu_ynBn7wpFNpFaCrAiF6wJpZAjbWXwuJ-dBEXHq8GnbvaHmTBjauT-af0NetcEIiIKIHU272x2JYg8rcnAt907nXXxQXqgG_zoZFwA5v4Pgo6C
Note: We used the private-key file and authority.datalog from above.
To check the contents of the Biscuit token, use this command.
> biscuit inspect -
Please input a base64-encoded biscuit, followed by <enter> and ^D
// Input the biscuit key obtained from the step above
EnYKDBgDIggKBggKEgIYDRIkCAASIHTT7y36m_zlF2BqdHkkXbu8no8u8tXQI06_BPmW2v17GkCnqmi4dKgkZX5b45542A5Ksuu_ynBn7wpFNpFaCrAiF6wJpZAjbWXwuJ-dBEXHq8GnbvaHmTBjauT-af0NetcEIiIKIHU272x2JYg8rcnAt907nXXxQXqgG_zoZFwA5v4Pgo6C
Authority block:
== Datalog ==
user("admin");
== Revocation id ==
763fb15e229364e7ad3e10a73a9f3a1929def5dbb1e2628c243a488b2c3e926ac45e3c7dc7d6b6106ca19a427e48367b83ce4a35c17d9a404d9459a551f9fa03
==========
š Public key check skipped š
š Datalog check skipped š”ļø
The datalog section confirms our authorization block. It also generates a revocation ID that one can use to invalidate the token if required
Check Biscuit Token Authority
To check the authority (allowed permissions) of an incoming Biscuit token (created above), we must create a set of rules to check against given facts. If the biscuit token that comes in follows the rules, the system allows the user to use the resource. In short, we need to check if a token is allowed to do the requested operation or not.
For that, we need to create another Datalog file. Let’s call it authorizer.datalog
// Facts 1
user("admin");
time(2021-12-21T20:00:00Z);
request("post");
// Facts2
resource("website");
backend("golang");
operation("create");
// Facts 3: server-side ACLs
permission("admin", "website", "delete");
permission("admin", "website", "create");
permission("admin", "blog", "delete");
// Condition
is_allowed($user, $res, $op) <-
user($user),
resource($res),
operation($op),
permission($user, $res, $op);
// allow/deny conditions
allow if is_allowed($user, $resource, $op);
There are 3 facts blocks in the above file
- Fact 1: Facts obtained from Biscuit token. Let’s say the biscuit token was sent to the authorizer server application using a Post request. Then, the application can get this information, File contents (User(“admin”)), time of the request, and type of request (Post, in our case).
- Facts 2: Hardcoded facts from the server application. This can come in handy when you need to apply checks and conditions based on a particular server.
- Facts 3: You can pull dynamic facts from external sources like databases. In our example above, ACLs are listed.
After the Facts, we have a allow/deny condition, which checks if all the rules are followed, and the user is authorized.
Let’s see how we can authorize a user with the CLI
> biscuit inspect - --verify-with-file authorizer.datalog --public-key 568e1a3876a327444f18414e66714d5deb908ba0b667ca9587bf9a03df8af478
Please input a base64-encoded biscuit, followed by <enter> and ^D
//Input the obtained biscuit token above
EnYKDBgDIggKBggKEgIYDRIkCAASIHTT7y36m_zlF2BqdHkkXbu8no8u8tXQI06_BPmW2v17GkCnqmi4dKgkZX5b45542A5Ksuu_ynBn7wpFNpFaCrAiF6wJpZAjbWXwuJ-dBEXHq8GnbvaHmTBjauT-af0NetcEIiIKIHU272x2JYg8rcnAt907nXXxQXqgG_zoZFwA5v4Pgo6C
Authority block:
== Datalog ==
user("admin");
== Revocation id ==
a7aa68b874a824657e5be39e78d80e4ab2ebbfca7067ef0a4536915a0ab02217ac09a590236d65f0b89f9d0445c7abc1a76ef6879930636ae4fe69fd0d7ad704
==========
ā
Public key check succeeded š
ā
Authorizer check succeeded š”ļø
Matched allow policy: allow if is_allowed($user, $resource, $op)
After the rules and facts in the authorizer.datalog file were checked against the rules, the system passed the authorizing rules, and gave the user permission. Now we will learn how to attenuate the biscuit tokens.
Biscuit Token Attenuation
Attenuation is the process of granting a user certain permissions lower than those of a current user in the hierarchy. The permissions will always be less than those of the current user. Attenuation is achieved by appending a new permission (policy) block to the existing biscuit. This process generates a new biscuit with less permission than the earlier one. Let’s see how to do it.
We will make a new attenuated biscuit token with a new rule to check if the user is a moderator. Create a new file, pass-attenuated.datalog, and paste the following content.
check if operation($operation), $operation.matches("create");
Remember, to make attenuated biscuits, we need to add new conditions to the existing biscuit file.
Now let’s create the attenuated biscuit authorization token using the above file
> biscuit attenuate - --block-file 'pass-attenuated.datalog'
Please input a base64-encoded biscuit, followed by <enter> and ^D
EnYKDBgDIggKBggKEgIYDRIkCAASIHTT7y36m_zlF2BqdHkkXbu8no8u8tXQI06_BPmW2v17GkCnqmi4dKgkZX5b45542A5Ksuu_ynBn7wpFNpFaCrAiF6wJpZAjbWXwuJ-dBEXHq8GnbvaHmTBjauT-af0NetcEIiIKIHU272x2JYg8rcnAt907nXXxQXqgG_zoZFwA5v4Pgo6C
// Ouput - New attenuated biscuit token
EnYKDBgDIggKBggKEgIYDRIkCAASIHTT7y36m_zlF2BqdHkkXbu8no8u8tXQI06_BPmW2v17GkCnqmi4dKgkZX5b45542A5Ksuu_ynBn7wpFNpFaCrAiF6wJpZAjbWXwuJ-dBEXHq8GnbvaHmTBjauT-af0NetcEGpkBCi8KBmNyZWF0ZRgDMiMKIQoCCBsSBggDEgIIAxoTCgQKAggDCgUKAxiACAoEGgIIBRIkCAASINK_87K2NdXFaQVCA2Dq-_fh-DE6i9Y9NJmlxOVB2kSZGkDOCWoLe4GR_TBrx4Y8S_WEM4Q77NWCyFtKeDW78lInHMWfkQ7zWfYwDzcJGdVoYw_52wxv67SI660cEZ8ETrUOIiIKIJGKPgwGyDwJkJS61HTrfxlcLV83BjTO5tav64Xv5UEw
So now you have a new attenuated biscuit token. Let’s verify the new token and see its content using the command we ran earlier
> biscuit inspect -
Please input a base64-encoded biscuit, followed by <enter> and ^D
// Input the new attenuated biscuit token
EnYKDBgDIggKBggKEgIYDRIkCAASIHTT7y36m_zlF2BqdHkkXbu8no8u8tXQI06_BPmW2v17GkCnqmi4dKgkZX5b45542A5Ksuu_ynBn7wpFNpFaCrAiF6wJpZAjbWXwuJ-dBEXHq8GnbvaHmTBjauT-af0NetcEGpkBCi8KBmNyZWF0ZRgDMiMKIQoCCBsSBggDEgIIAxoTCgQKAggDCgUKAxiACAoEGgIIBRIkCAASINK_87K2NdXFaQVCA2Dq-_fh-DE6i9Y9NJmlxOVB2kSZGkDOCWoLe4GR_TBrx4Y8S_WEM4Q77NWCyFtKeDW78lInHMWfkQ7zWfYwDzcJGdVoYw_52wxv67SI660cEZ8ETrUOIiIKIJGKPgwGyDwJkJS61HTrfxlcLV83BjTO5tav64Xv5UEw
Authority block:
== Datalog ==
user("admin");
== Revocation id ==
a7aa68b874a824657e5be39e78d80e4ab2ebbfca7067ef0a4536915a0ab02217ac09a590236d65f0b89f9d0445c7abc1a76ef6879930636ae4fe69fd0d7ad704
==========
Block nĀ°1:
== Datalog ==
check if operation($operation), $operation.contains("create");
== Revocation id ==
ce096a0b7b8191fd306bc7863c4bf58433843becd582c85b4a7835bbf252271cc59f910ef359f6300f370919d568630ff9db0c6febb488ebad1c119f044eb50e
==========
š Public key check skipped š
š Datalog check skipped š”ļø
If you observe the output thoroughly, you will notice a new block in the Token confirming the attenuation token
Block nĀ°1:
== Datalog ==
check if operation($operation), $operation.contains("create");
Checking the Attenuated Biscuit Token
Let’s finally check if the new Attenuated Biscuit token is verified on the server. We can run the command that we already know
> biscuit inspect - --verify-with-file authorizer.datalog --public-key 568e1a3876a327444f18414e66714d5deb908ba0b667ca9587bf9a03df8af478
Please input a base64-encoded biscuit, followed by <enter> and ^D
// Enter the new biscuit attenuated token
EnYKDBgDIggKBggKEgIYDRIkCAASIHTT7y36m_zlF2BqdHkkXbu8no8u8tXQI06_BPmW2v17GkCnqmi4dKgkZX5b45542A5Ksuu_ynBn7wpFNpFaCrAiF6wJpZAjbWXwuJ-dBEXHq8GnbvaHmTBjauT-af0NetcEGpkBCi8KBmNyZWF0ZRgDMiMKIQoCCBsSBggDEgIIAxoTCgQKAggDCgUKAxiACAoEGgIIBRIkCAASINK_87K2NdXFaQVCA2Dq-_fh-DE6i9Y9NJmlxOVB2kSZGkDOCWoLe4GR_TBrx4Y8S_WEM4Q77NWCyFtKeDW78lInHMWfkQ7zWfYwDzcJGdVoYw_52wxv67SI660cEZ8ETrUOIiIKIJGKPgwGyDwJkJS61HTrfxlcLV83BjTO5tav64Xv5UEw
Authority block:
== Datalog ==
user("admin");
== Revocation id ==
a7aa68b874a824657e5be39e78d80e4ab2ebbfca7067ef0a4536915a0ab02217ac09a590236d65f0b89f9d0445c7abc1a76ef6879930636ae4fe69fd0d7ad704
==========
Block nĀ°1:
== Datalog ==
check if operation($operation), $operation.contains("create");
== Revocation id ==
ce096a0b7b8191fd306bc7863c4bf58433843becd582c85b4a7835bbf252271cc59f910ef359f6300f370919d568630ff9db0c6febb488ebad1c119f044eb50e
==========
ā
Public key check succeeded š
ā
Authorizer check succeeded š”ļø
Matched allow policy: allow if is_allowed($user, $resource, $op)
Result: Pass
As you can see, the server successfully verified the attenuated token. Run a few tests with different parameters on the pass-attenuated.datalog file and check what all parameters are allowed by the authorizer.
Test
Try creating a new attenuated biscuit with the following details and see if it passes
check if user($user), $user.contains("moderator");
Your test should result in a failure or else something was wrong in your rules or facts
Download all the files from GitHub
Conclusion
Biscuit authorization tokens are amazing authorization tokens for Microservices architecture. It helps you save a lot of time when implementing authorization while also providing top-class security aspects. In this tutorial, we learned the basics of Biscuits and saw how to use the Biscuit CLI.