Biscuit Authorization Part I

Creating a Biscuit with a CLI and checking the authority of the token.

Chilarai Mushahary

Software Engineer

Space and Time relies on biscuits to support decentralized authorization. On the Space and Time platform, Biscuits are used to authorize access to resources (e.g. tables). To learn more about how Biscuits are used in Space and Time, visit our developer docs or check out this video.

In this tutorial, we’ll learn about Biscuit authorization and how to create, authorize, and attenuate the tokens. Download all the files from GitHub here.

It’s often painful to write and check authorization codes on each and 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.

Biscuit authorization diagram

Long tutorial alert!

This tutorial is a part of 2 tutorial series.

  1. Part 1: 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.
  2. 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 you don'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.

Biscuit offline attenuation
Biscuit offline attenuation

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.

You 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.

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, then 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 “Users” are from different namespaces and do not collide when writing rules.

Trying the Command Line Interface (CLI)

The Biscuit 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 keypair

Once you install the CLI, you can create a basic public/private keypair for your application. The server stores the public key, and it can use it to verify any token signed by the private key. Example:

> 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:

  1. Facts: Hardcoded data like user(["admin", "moderator"])
  2. Rules: To generate more facts based on conditions. Explained later.
  3. 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 you 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 need to 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:

  1. 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).
  2. 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.
  3. Facts 3: You can pull dynamic facts from external sources like databases. In our example above, ACLs are listed.

After the Facts, we have an 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");

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, in order 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.

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.

Chilarai Mushahary

Software Engineer

Chilarai works with Space and Time as a software engineer contractor. He has more than a decade of experience developing products and creating ecosystems around them. He has previously worked with network security and analytics firms on building tools to process huge volumes of data on desktop and cloud servers. Chilarai is also an avid open-source advocate and contributor.