Biscuit 3.0

Posted March 28, 2023 by clementd ‐ 8 min read

The Biscuit reference implementation in Rust has been released, with full support for the latest spec additions

Biscuit is a specification for a cryptographically verified authorization token supporting offline attenuation, and a language for authorization policies based on Datalog. It is used to build decentralized authorization systems, such as microservices architectures, or advanced delegation patterns with user facing systems. After 5 years of development, Biscuit is now at its third major version, opening up new patterns of authorization.

The biscuit specification and the library ecosystem

Biscuit is published as specification, along with several implementations. Amongst them, biscuit-rust is the reference implementation. It is used when programming in rust, but it also powers biscuit-wasm which provides support for javascript applications (both client-side and server-side), and biscuit-web-components which provides in-browser interactive tooling.

Biscuit v3

Motivated by third-party blocks, a new specification version was released. In addition to third-party blocks, a few other changes have been included in the new version.

  • support for check all
  • support for bitwise operators
  • support for !=
  • support for authorizer snapshots

A word on biscuit versions

The currently supported biscuit versions are v2 and v3. Those two versions are compatible and v3 is opt-in: if you don't use any new feature from biscuit v3, the generated tokens will work with implementations only supporting v2 biscuits. Of course, v2 biscuits continue being supported by new implementations. It is even possible to attenuate a v2 biscuit with v3 features.

biscuit-rust v3.0.0

The last stable release of biscuit-rust was v2.2.0, published ten months ago. While support for third-party blocks and other v3 features make for the bulk of the changes, the library itself has been improved. The biggest improvement would be the datalog macros (introduced in v2.2.0) which are now covering all use-cases in a performant fashion.

Third-party blocks

Third-party blocks allow cross-domain authorization with fine-grained access rules. This is an exciting development, since it makes distributed auth patterns possible with no out-of-band synchronisation required: all required information can be carried in a single biscuit token.

See more on the post introducing third-party blocks.

// authority block, emitted by the login service.
// it only allows read access to the `file1` resource
user("clementd");
right("file1", "read");
// the fact `member("pink-floyd-fans")`; has to come from a block
// signed by the social network: this is enforced via the public key check
// so the token is effectively inert as long as it is not augmented by the
// social network service.
check if member("pink-floyd-fans") trusting ed25519/398ad724c0da3756bb8709a85179a3ff9c34f8ec08317c3d8f79d75db7eab014;

// block provided by the social network
// it provides the proof that the holder is part of the pink-floyd-fans group, but adds
// a check: the proof is valid only for a limited amount of time
// this block carries a signature that can be verified with the following
// public key: 398ad724c0da3756bb8709a85179a3ff9c34f8ec08317c3d8f79d75db7eab014
member("pink-floyd-fans");
check if time($time), $time < 2022-08-01T00:00;

// verifier policy.
allow if right("file1", "read");

Datalog macros

Biscuit comes with a powerful DSL for authorization rules. A common pain point with using DSLs is using dynamic values within an embedded program. Concatenating strings opens the door to nasty security issues. SQL injections are the infamous example of this kind of problem. Another frustrating issue (at least for rust developers) is that when an embedded program is defined in a regular string, it becomes opaque up until it is parsed at runtime. So we get runtime errors on static elements, doubled with performance issues.

biscuit-rust provides a solution to both these issues:

  • a parameter injection syntax allows splicing dynamic values with no risk of injections;
  • a series of macros move datalog parsing to compile-time, providing both static guarantees and performant code.
let user_id = "1234";
let expiration = SystemTime::now() + Duration::from_secs(86400);
let mut authority = biscuit!(r#"
  user({user_id});
  check if time($time), $time < {expiration};
  "#
);

let rights = ["read", "write"];
for right in rights {
  biscuit_merge!(&mut authority, r#"
    right({right});
  "#);
}

Do note that the parameter injection syntax is also available for runtime parsing:

let user_id = "1234";
let expiration = SystemTime::now() + Duration::from_secs(86400);
let mut params = HashMap::new();
params.insert("user_id".to_string(), user_id.into());
params.insert("expiration".to_string(), expiration.into());

let mut authority = BiscuitBuilder::new();
authority.add_code_with_params(
    r#" user({user_id}); check if time($time), $time < {expiration}; "#,
    params,
    HashMap::new(),
)?;

let rights = ["read", "write"];
for right in rights {
    let mut fact: Fact = r#"right({right})"#.try_into()?;
    fact.set("right", right)?;
    authority.add_fact(fact)?;
}

This feature is what makes possible to provide safe interpolation in biscuit- wasm.

Authorizer snapshots

Another addition to the biscuit spec is authorizer snapshots. It is possible to save the current state of an authorizer to a file, and then to load it back.

This allows authorizer evaluation: create an authorizer, pre-evaluate rules, store it somewhere, and then resume execution once you have more context. This can be useful in systems that enforce a strict authentication/authorization separation.

Another use-case is post-hoc inspection. After performing authorization, you can dump the full context to a file, and inspect it with the CLI afterwards. If you have ever tried to debug authorization issues after the fact, then you might find this useful.

> biscuit inspect-snapshot snapshot-file
time(2023-03-30T09:39:39Z);
user("1234");
right("file1", "read");
time(2023-03-30T09:39:39Z);

check if time($time), $time < 2023-03-31T00:00:00Z;

allow if right("file2", "read");

biscuit-wasm v0.4.0

In addition to the biscuit-rust release, a release of biscuit-wasm is planned for the upcoming days. biscuit-wasm-0.4.0 will bundle all of the improvements from biscuit-rust-3.0.0 and will also pack JS-specific improvements. The most important one is the JS counterpart to datalog macros in rust: tagged templates.

This is javascript, so there is no way to move datalog parsing to compile-time, but at least parameter injection is handled nicely.

let user_id = "1234";
let expiration = new Date(new Date().getTime() + 86400000);
let authority =
  biscuit`user(${user_id});
          check if time($time), $time < ${expiration}`;

for (let right of ["read", "write"]) {
   authority.add_fact(fact`right(${right})`);  
}

let token = authority.build(secretKey);
let auth = authorizer`time(${new Date()}); allow if user($u);`;
auth.authorize();
let facts = auth.query(rule`u($id) <- user($id)`);

Other ongoing projects

While biscuit-rust and biscuit-wasm are where most of the work happens, there are a few areas where things have happened.

  • biscuit-cli allows manipulating and inspecting biscuits on the command line;
  • biscuit-dotnet has been released recently, providing biscuit support to the dotnet platform;
  • biscuit-web-components provide an embeddable editor and token inspector.

Syntax highlighting with tree-sitter

Tree-sitter allows to provide syntax highlighting and more advanced features (AST-based navigation) in a cross-editor way. There are plugins for emacs and neovim, and modern editors like helix support it out of the box. Not only this makes editing biscuit-datalog files more convenient, but when paired with language injection, it also means that editing a datalog snippet inside your programming language benefits from syntax highlighting.

A code editor with a datalog program in it

Typing the contents of a biscuit token from within a code editor

A datalog block within a markdown document

Datalog syntax highlighting within a markdown code fence

A datalog snippet inside a javascript typed template string

Datalog syntax highlighting within javascript tagged template string

In addition to the editor-agnostic grammar, editor-specific queries are needed for highlighting and AST manipulation. As of now, those queries are only written for the helix editor, but it is a fun project to add support for other editors. Reach out to clementd on matrix if you want to try!

A word on Outscale

I (Clément Delafargue) have joined Outscale last week, and working on biscuit is now a part of my job, since Outscale is investing in biscuit. That's great news since it allows me to dedicate way more time to biscuit. Some of my coworkers will also contribute to biscuit, so we are planning to carry out improvements in several areas, including tutorials and documentation.

Outscale is a French IaaS cloud provider, a brand of Dassault Systèmes, providing services to customers with strong data security and sovereignity constraints.

Let's have a chat!

Please come have a chat on our matrix room if you have questions about biscuit. There is a lot to discover!