Security

Biscuit tokens

Biscuit authorization tokens: issued with Datalog facts and checks signed by your key, narrowed by their holder without asking the issuer, and authorized offline against each request.

Version 1.0.0 · 6 functions · 1 crate

#auth#tokens#api#signing

Functions

What use biscuit.qwer gives a program, with the types Quillo sees. To take only some, or under other names: use biscuit_keypair, biscuit_public_key from biscuit (Quillo 0.6).

biscuit_keypair() → any

A new root key pair: { private, public }, as ed25519-private/<hex> and ed25519/<hex>. Keep the private key on the server that issues tokens; give the public one to every service that checks them.

biscuit_public_key(private_key: str) → stror an error

The public key of a private one.

biscuit_issue(private_key: str, code: str, params: map) → stror an error

A token signed with private_key holding code, Datalog facts, rules and checks (user({id}); right("invoices", "read"); check if time($t), $t < {expires};). {name} in the code is filled from params, safely quoted: strings, numbers, booleans, and dates written 2026-11-01T12:00:00Z. Gives the token in URL-safe base64.

biscuit_attenuate(token: str, code: str, params: map) → stror an error

token narrowed by a block of checks (check if operation("read");), with {name} filled from params. Anyone holding a token can do this, without the issuer's key: a narrowed token can only do less.

biscuit_authorize(token: str, public_key: str, code: str, params: map) → anyor an error

Checks token against public_key and an authorizer's code: the request's facts (resource({id}); operation("read");) and policies (allow if right({id}, "read");), {name} filled from params, the current time added as time(...). Gives { allowed: true, policy } (the index of the allow policy that matched), or an error value naming the checks that failed.

biscuit_inspect(token: str) → anyor an error

What a token holds, without checking its signature: { blocks: [<Datalog source of each block>], revocation_ids }. revocation_ids (hex, one per block) go in a deny list to revoke a token and every token narrowed from it.

Get it

$ quillo install biscuit               # biscuit.qwer into this folder$ quillo install biscuit --example     # and its example, biscuit.qwe

Or with curl:

$ curl -O https://quillolang.org/plugins/files/biscuit.qwer$ curl -O https://quillolang.org/plugins/files/biscuit.qwe

Then, in your program, all of it or only what you call:

main.qwe
use biscuit.qweruse biscuit_keypair, biscuit_public_key from biscuit      // Quillo 0.6: only these

It uses this crate, which cargo fetches the first time the plugin is built:

[dependencies]
biscuit-auth = "6"

Example

biscuit.qwe
// An invoicing API that hands out Biscuit tokens: the token says what its holder may do, a// partner narrows its own to read-only before passing it to a reporting tool, and each request// is authorized from the token alone.////   BISCUIT_PRIVATE_KEY=... quillo run biscuit.qwe      then open http://localhost:8080/demo//// Without BISCUIT_PRIVATE_KEY a fixed demo key is used: make your own with biscuit_keypair().use "biscuit.qwer"let PRIVATE = env("BISCUIT_PRIVATE_KEY", "ed25519-private/7c4b1f0dc9a5e3f6b1d2c3e4f5a6b7c8d9e0f1a2b3c4d5e6f7a8b9c0d1e2f3a4")let PUBLIC = biscuit_public_key(PRIVATE)?let ISSUE = `    user({user});    tenant({tenant});    right("invoices", "read");    right("invoices", "write");    check if time($t), $t < {expires};`let READ_ONLY = `check if operation("read");`// The service's side: the request's facts, then the policy.let AUTHORIZE = `    resource("invoices");    operation({op});    tenant_requested({tenant});    check if tenant($t), tenant_requested($t);    allow if right("invoices", {op});`fn allowed(token, op, tenant) = biscuit_authorize(token, PUBLIC, AUTHORIZE, { op, tenant })route GET /demo {    let token = biscuit_issue(PRIVATE, ISSUE, { user: "u42", tenant: "acme", expires: "2099-01-01T00:00:00Z" })?    let reporting = biscuit_attenuate(token, READ_ONLY, {})?    let expired = biscuit_issue(PRIVATE, ISSUE, { user: "u42", tenant: "acme", expires: "2020-01-01T00:00:00Z" })?    {        write_full: allowed(token, "write", "acme")?,        read_narrowed: allowed(reporting, "read", "acme")?,        write_narrowed: allowed(reporting, "write", "acme").error,        other_tenant: allowed(token, "read", "globex").error,        expired: allowed(expired, "read", "acme").error,        forged: biscuit_authorize(token, biscuit_keypair().public, AUTHORIZE, { op: "read", tenant: "acme" }).error,    }}route GET /inspect {    let token = biscuit_issue(PRIVATE, ISSUE, { user: "u42", tenant: "acme", expires: "2099-01-01T00:00:00Z" })?    biscuit_inspect(biscuit_attenuate(token, READ_ONLY, {})?)?}
$ quillo run biscuit.qwe$ curl 'localhost:8080/demo'$ curl 'localhost:8080/inspect'

Source

biscuit.qwer
//! Biscuit authorization tokens: issued with Datalog facts and checks signed by your key, narrowed by whoever holds one (read-only, one tenant, an hour) without asking the issuer, and authorized offline against each request's policies.//! version: 1.0.0//!//! [dependencies]//! biscuit-auth = "6"use biscuit_auth::builder::{AuthorizerBuilder, BlockBuilder, Term};use biscuit_auth::{Biscuit, KeyPair, PrivateKey, PublicKey, UnverifiedBiscuit};use serde_json::{json, Value};use std::collections::HashMap;use std::str::FromStr;fn params(p: &HashMap<String, Value>) -> Result<HashMap<String, Term>, String> {    p.iter()        .map(|(k, v)| {            let t = match v {                Value::String(s) if s.len() == 20 && s.ends_with('Z') && s.as_bytes()[10] == b'T' => date(s).map(Term::Date).unwrap_or_else(|| Term::Str(s.clone())),                other => Term::try_from(other.clone()).map_err(|e| format!("parameter `{}`: {}", k, e))?,            };            Ok((k.clone(), t))        })        .collect()}/// `2026-11-01T12:00:00Z` as Unix seconds.fn date(s: &str) -> Option<u64> {    let n = |a: usize, b: usize| s.get(a..b)?.parse::<i64>().ok();    let (y, m, d, hh, mm, ss) = (n(0, 4)?, n(5, 7)?, n(8, 10)?, n(11, 13)?, n(14, 16)?, n(17, 19)?);    let (y2, m2) = if m <= 2 { (y - 1, m + 9) } else { (y, m - 3) };    let era = y2.div_euclid(400);    let yoe = y2 - era * 400;    let doy = (153 * m2 + 2) / 5 + d - 1;    let doe = yoe * 365 + yoe / 4 - yoe / 100 + doy;    let days = era * 146097 + doe - 719468;    u64::try_from(days * 86400 + hh * 3600 + mm * 60 + ss).ok()}fn private(key: &str) -> Result<KeyPair, String> {    let k = PrivateKey::from_str(key.trim()).map_err(|e| format!("the private key: {} (it looks like ed25519-private/<hex>)", e))?;    Ok(KeyPair::from(&k))}fn public(key: &str) -> Result<PublicKey, String> {    PublicKey::from_str(key.trim()).map_err(|e| format!("the public key: {} (it looks like ed25519/<hex>)", e))}fn err(e: biscuit_auth::error::Token) -> String {    e.to_string()}/// A new root key pair: `{ private, public }`, as `ed25519-private/<hex>` and `ed25519/<hex>`. Keep the private key on the server/// that issues tokens; give the public one to every service that checks them.pub fn biscuit_keypair() -> Value {    let kp = KeyPair::new();    json!({ "private": kp.private().to_prefixed_string(), "public": kp.public().to_string() })}/// The public key of a private one.pub fn biscuit_public_key(private_key: &str) -> Result<String, String> {    Ok(private(private_key)?.public().to_string())}/// A token signed with `private_key` holding `code`, Datalog facts, rules and checks (`user({id}); right("invoices", "read");/// check if time($t), $t < {expires};`). `{name}` in the code is filled from `params`, safely quoted: strings, numbers, booleans,/// and dates written `2026-11-01T12:00:00Z`. Gives the token in URL-safe base64.pub fn biscuit_issue(private_key: &str, code: &str, params: std::collections::HashMap<String, serde_json::Value>) -> Result<String, String> {    let kp = private(private_key)?;    Biscuit::builder()        .code_with_params(code, self::params(&params)?, HashMap::new())        .map_err(err)?        .build(&kp)        .map_err(err)?        .to_base64()        .map_err(err)}/// `token` narrowed by a block of checks (`check if operation("read");`), with `{name}` filled from `params`. Anyone holding/// a token can do this, without the issuer's key: a narrowed token can only do less.pub fn biscuit_attenuate(token: &str, code: &str, params: std::collections::HashMap<String, serde_json::Value>) -> Result<String, String> {    let t = UnverifiedBiscuit::from_base64(token.trim()).map_err(err)?;    let block = BlockBuilder::new().code_with_params(code, self::params(&params)?, HashMap::new()).map_err(err)?;    t.append(block).map_err(err)?.to_base64().map_err(err)}/// Checks `token` against `public_key` and an authorizer's `code`: the request's facts (`resource({id}); operation("read");`)/// and policies (`allow if right({id}, "read");`), `{name}` filled from `params`, the current time added as `time(...)`./// Gives `{ allowed: true, policy }` (the index of the allow policy that matched), or an error value naming the checks that failed.pub fn biscuit_authorize(token: &str, public_key: &str, code: &str, params: std::collections::HashMap<String, serde_json::Value>) -> Result<Value, String> {    let t = Biscuit::from_base64(token.trim(), public(public_key)?).map_err(err)?;    let mut a = AuthorizerBuilder::new().code_with_params(code, self::params(&params)?, HashMap::new()).map_err(err)?.time().build(&t).map_err(err)?;    let policy = a.authorize().map_err(err)?;    Ok(json!({ "allowed": true, "policy": policy }))}/// What a token holds, without checking its signature: `{ blocks: [<Datalog source of each block>], revocation_ids }`./// `revocation_ids` (hex, one per block) go in a deny list to revoke a token and every token narrowed from it.pub fn biscuit_inspect(token: &str) -> Result<Value, String> {    let t = UnverifiedBiscuit::from_base64(token.trim()).map_err(err)?;    let blocks: Result<Vec<String>, String> = (0..t.block_count()).map(|i| t.print_block_source(i).map_err(err)).collect();    let ids: Vec<String> = t.revocation_identifiers().iter().map(|b| b.iter().map(|x| format!("{:02x}", x)).collect()).collect();    Ok(json!({ "blocks": blocks?, "revocation_ids": ids }))}
Thread
78Useful? Like it, or add to its thread below.

Thread 0 posts

On Biscuit tokens: questions, tips, corrections, and code that uses it.

No posts yet. The first one opens the thread.