ptondereau/biscuit-symfony-bundle

Symfony bundle for Biscuit authorization tokens

Maintainers

Package info

github.com/ptondereau/biscuit-sf-bundle

Type:symfony-bundle

pkg:composer/ptondereau/biscuit-symfony-bundle

Statistics

Installs: 0

Dependents: 0

Suggesters: 0

Stars: 0

Open Issues: 0

v0.1.0 2026-05-06 14:41 UTC

This package is auto-updated.

Last update: 2026-05-06 14:49:50 UTC


README

Symfony bundle for Biscuit authorization tokens.

CI Coverage Status Latest Version PHP Version License

About

Biscuit is a bearer token format with offline attenuation, third-party blocks, and a Datalog-based authorization language. This bundle integrates Biscuit into Symfony's Security component so you can authenticate requests carrying Biscuit tokens and enforce policies through the standard #[IsGranted] attribute.

What you get:

  • Token extraction from Authorization header and/or cookies
  • Symfony authenticator that validates the token's signature against your public key
  • A BiscuitVoter that runs your Datalog policies against the request, fully driven by #[IsGranted]
  • Configurable token caching and revocation checking
  • A web profiler panel showing the current token, its blocks, and every policy decision
  • Console commands to generate keys, mint tokens from templates, and inspect tokens
  • A make:biscuit-policy maker
  • Test helpers to mint tokens and authenticate functional tests

Read the Datalog reference at biscuitsec.org/docs/reference/datalog.

Requirements

  • PHP 8.1 or higher
  • Symfony 6.4, 7.4, or 8.0
  • The biscuit-php PHP extension (version 0.4.0)

Installation

Install the PHP extension via pie:

pie install ptondereau/biscuit-php:0.4.0

Install the bundle via Composer:

composer require ptondereau/biscuit-symfony-bundle

If you are not using Symfony Flex, register the bundle manually in config/bundles.php:

return [
    // ...
    Biscuit\BiscuitBundle\BiscuitBundle::class => ['all' => true],
];

Quick Start

Generate a key pair:

bin/console biscuit:keys:generate

Configure the bundle (config/packages/biscuit.yaml):

biscuit:
    keys:
        public_key: '%env(BISCUIT_PUBLIC_KEY)%'
        private_key: '%env(BISCUIT_PRIVATE_KEY)%'
    policies:
        admin_only: 'allow if role("admin")'

Wire the authenticator into your firewall (config/packages/security.yaml):

security:
    firewalls:
        api:
            pattern: ^/api
            stateless: true
            custom_authenticators:
                - biscuit.authenticator

Protect a controller with #[IsGranted]:

use Symfony\Component\Routing\Attribute\Route;
use Symfony\Component\Security\Http\Attribute\IsGranted;

final class AdminController
{
    #[Route('/api/admin', methods: ['GET'])]
    #[IsGranted('admin_only')]
    public function index(): Response
    {
        return new JsonResponse(['ok' => true]);
    }
}

That's it. Requests without a valid token get 401, requests whose token does not satisfy the policy get 403.

Configuration Reference

biscuit:
    keys:
        public_key:        ~       # Public key in hex
        private_key:       ~       # Private key in hex
        public_key_file:   ~       # Path to public key file (alternative to public_key)
        private_key_file:  ~       # Path to private key file (alternative to private_key)
        algorithm:         ed25519 # ed25519 or secp256r1

    security:
        token_extractor:
            header: true     # Extract token from Authorization header
            cookie: false    # Cookie name to read from, or false to disable

    cache:
        enabled: false       # Enable token verification caching
        pool:    cache.app   # Cache pool service ID
        ttl:     3600        # Cache TTL in seconds

    revocation:
        enabled: false       # Enable token revocation checking
        service: ~           # Revocation checker service ID

    policies:                # Named policies referenced by #[IsGranted]
        admin_only:    'allow if role("admin")'
        scope_read:    'allow if scope({resource}, "read")'

    token_templates:         # Templates used by BiscuitTokenFactory and biscuit:token:create
        admin_token:
            facts:
                - 'user({id})'
                - 'role("admin")'
            checks:
                - 'check if time($t), $t < {expiry}'
            rules: []

Key Management

The KeyManager service exposes the configured key pair. Three input forms are supported:

  • Inline hex via keys.public_key and keys.private_key (recommended via env vars).
  • Files via keys.public_key_file and keys.private_key_file.
  • A new pair generated on demand if none is configured (only useful in tests).

To generate a fresh pair from the CLI:

bin/console biscuit:keys:generate --algorithm=ed25519

The command prints the hex-encoded keys to stdout. Store them in your secrets manager and inject them via environment variables.

Token Extraction

Tokens are extracted by the ChainTokenExtractor, which delegates to one or more named extractors in order:

  • HeaderTokenExtractor reads Authorization: Bearer <token> from the request.
  • CookieTokenExtractor reads a configurable cookie name.

Enable both:

biscuit:
    security:
        token_extractor:
            header: true
            cookie: biscuit_token

The chain stops at the first extractor that returns a non-null value. To add a custom extractor, implement Biscuit\BiscuitBundle\Token\Extractor\TokenExtractorInterface and register it as a service tagged into the chain.

Authentication

The bundle ships a single authenticator: BiscuitAuthenticator. Add it as a custom_authenticators entry on any stateless firewall that should accept Biscuit tokens.

A successful authentication produces a BiscuitUser whose getBiscuit() method returns the verified token, which is then available throughout the request. Failed authentication throws RevokedTokenException (when revocation is enabled and the token is on the revocation list) or returns a generic 401 for invalid signatures, malformed tokens, and missing extractors.

The BiscuitBadge is attached to the security passport so downstream voters and listeners can detect Biscuit-authenticated requests.

Authorization

Policies are referenced by name from #[IsGranted]:

biscuit:
    policies:
        admin_only:   'allow if role("admin")'
        owner_only:   'allow if user({user_id})'
        scope_read:   'allow if scope({resource}, "read")'
#[IsGranted('admin_only')]
public function dashboard(): Response { /* ... */ }

#[IsGranted('owner_only', subject: ['user_id' => $userId])]
public function profile(int $userId): Response { /* ... */ }

#[IsGranted('scope_read', subject: $resource)]
public function show(string $resource): Response { /* ... */ }

The voter resolves the policy and runs an authorizer over the verified token. The subject: argument is bound into the policy as parameters:

  • A string subject becomes {resource} in the policy.
  • An object with getId() becomes {resource} as the string-cast id.
  • An associative array is bound key-by-key (use this for multi-parameter policies).

You can also pass a Datalog string directly to #[IsGranted] for ad-hoc policies that aren't worth naming:

#[IsGranted('allow if scope({resource}, "read")', subject: $resource)]

If the configured policies do not match, the voter abstains and falls back to other voters.

Token Templates

Define reusable token shapes in configuration:

biscuit:
    token_templates:
        admin_token:
            facts:
                - 'user({id})'
                - 'role("admin")'
        scoped_reader:
            facts:
                - 'user({id})'
                - 'scope({resource}, "read")'

Mint tokens from templates with BiscuitTokenFactory:

use Biscuit\BiscuitBundle\Token\BiscuitTokenFactory;

final class IssueTokenAction
{
    public function __construct(private readonly BiscuitTokenFactory $factory) {}

    public function __invoke(int $userId, string $dog): string
    {
        $token = $this->factory->create('scoped_reader', [
            'id' => $userId,
            'resource' => $dog,
        ]);

        return $token->toBase64();
    }
}

Console Commands

Command Purpose
biscuit:keys:generate Generate an ed25519 or secp256r1 key pair
biscuit:token:create Mint a token from a configured template
biscuit:token:inspect Decode and pretty-print a Biscuit token
biscuit:policy:test Run a configured policy against a token

Each command exposes --help for the full option list.

Maker

If you also have symfony/maker-bundle installed:

bin/console make:biscuit-policy ArticleViewerPolicy

This generates src/Security/Policy/ArticleViewerPolicy.php with a documented skeleton including a NAME constant, a POLICY Datalog string, and a usage example with #[IsGranted].

Token Caching

For high-throughput APIs you can cache successful token verifications to avoid re-running signature checks on every request:

biscuit:
    cache:
        enabled: true
        pool: cache.app
        ttl: 600

Cache keys are derived from the serialized token. Failed verifications are never cached.

Token Revocation

To enforce a revocation list, implement Biscuit\BiscuitBundle\Cache\Revocation\RevocationCheckerInterface and wire it in:

biscuit:
    revocation:
        enabled: true
        service: App\Security\MyRevocationChecker

A reference implementation backed by a Symfony cache pool is provided as Biscuit\BiscuitBundle\Cache\Revocation\CacheRevocationChecker.

When revocation is enabled, the authenticator throws RevokedTokenException for any token whose revocation IDs intersect the revoked set. Revocation checks happen after signature verification but before policy evaluation.

Web Profiler Integration

When symfony/web-profiler-bundle is installed in the dev environment, the bundle adds a Biscuit panel showing:

  • Whether a token was attached to the request, with block count and revocation IDs
  • All blocks in the token (Datalog source)
  • Every policy check performed during the request, with parameters and pass/fail outcome

The toolbar shows a green/red indicator and the count of policy checks.

Testing Helpers

The Biscuit\BiscuitBundle\Test namespace provides utilities for functional tests.

BiscuitTestTrait mints tokens against a per-test-class key pair without needing to mock anything:

use Biscuit\BiscuitBundle\Test\BiscuitTestTrait;
use Symfony\Bundle\FrameworkBundle\Test\WebTestCase;

final class ProtectedEndpointTest extends WebTestCase
{
    use BiscuitTestTrait;

    public function testAdminCanAccess(): void
    {
        $token = $this->createTestTokenBase64('user(1); role("admin")');

        $client = static::createClient();
        $client->request('GET', '/api/admin', server: [
            'HTTP_AUTHORIZATION' => 'Bearer ' . $token,
        ]);

        self::assertResponseIsSuccessful();
    }
}

TestBiscuitAuthenticator is a drop-in replacement for the production authenticator that trusts tokens signed by the test key pair.

BiscuitFixtures and BiscuitFixtureLoader load Datalog scenarios from YAML files for repeatable fixture data.

Development

Run the full quality gate locally:

composer check

This runs php-cs-fixer (dry run), phpstan at level 8, and the PHPUnit suite.

To auto-fix style issues:

composer cs-fix

Contributing

See CONTRIBUTING.md. All contributors are expected to follow the Code of Conduct.

Security

Vulnerabilities should be reported to security@biscuitsec.org. See SECURITY.md for details.

License

Apache License 2.0. See LICENSE.