Installing the bundle¶
The bundle is installed as any Symfony bundle is, and most of what it needs from the other bundles of the kernel it writes itself. What it cannot know is the application's to write: the key management client, the issuer, the firewall, the path the routes are mounted under. Getting started is the short path through it; this page is the whole of it.
What it requires¶
| What | Version |
|---|---|
| PHP | 8.4 or later, with the intl and zip extensions |
| Symfony | 8.2 |
| Doctrine | ORM 3.7, DBAL 4.4, DoctrineBundle 3.3, Doctrine Migrations 3.9 |
| Database | PostgreSQL: the migrations the bundle ships are written for it and stop on any other platform |
| Key management | A client of symfony/key-management, declared by the application |
The kernel registers FrameworkBundle, DoctrineBundle, SecurityBundle, TwigBundle and the bundle
itself. To play the migrations it also registers DoctrineMigrationsBundle, which the standalone
application requires, and in development WebProfilerBundle shows the panel of the
provider.
Requiring and registering it¶
The package has no Symfony Flex recipe: the configuration, the routes and the firewall below are written by hand. The bundle is registered for every environment:
What the bundle writes for the other bundles¶
When the container is built, the bundle prepends its part to the configuration of the bundles it works with, and only of those the kernel registers. An application writes none of this, and a prepended configuration being merged first, a key the application writes itself is the one that stands.
| Extension | What the bundle adds |
|---|---|
doctrine |
Its DBAL types, and three XML mappings, BeffroiCore, BeffroiOidc and BeffroiSaml, for the model of the three packages. The types that seal a column are registered when the bundles boot |
doctrine_migrations |
The directory of its migrations, under the namespace Beffroi\Bundle\Migrations |
security |
The password hasher of Beffroi\Bundle\Security\SecurityUser, which is Beffroi\Bundle\Security\CorePasswordHasher, the hasher of the core |
rate_limiter |
Two limiters, beffroi_code_attempts and beffroi_password_attempts: five attempts per user in a sliding window of one minute |
framework |
Eight cache pools on cache.app: beffroi.client_keys, beffroi.issuer_keys, beffroi.client_assertions, beffroi.issuer_assertions, beffroi.software_statements, beffroi.request_objects, beffroi.dpop and beffroi.load |
messenger |
The three buses, their middleware, the routing of every message, the serializer and the transports |
scheduler |
use_messenger_routing: true, so that the recurring messages of the provider are routed by their class like every other message |
What the application writes¶
The key management client¶
The bundle seals with a client it does not build. The client is declared under key_management.clients,
and the bundle names it:
key_management:
clients:
op: 'service://beffroi.kms.op'
services:
beffroi.kms.op:
class: Symfony\Component\KeyManagement\EncrypterInterface
factory: ['@key_management.factory', 'fromString']
arguments: ['%env(BEFFROI_KMS_DSN)%']
The configuration of the bundle¶
Two sections are required: the tenant with its issuer, and the keys with the client that seals them and the master key they are sealed under. Every other section has a default, or is a feature the deployment does not serve until it names it.
beffroi:
tenant:
issuer: '%env(BEFFROI_ISSUER)%'
keys:
kms: op
key_id: '%env(BEFFROI_KMS_KEY_ID)%'
A kms that names no client of key_management.clients fails the build of the container, and the message
lists the clients that are registered.
The configuration, section by section
The routes¶
They are imported under the path of the issuer, which is no prefix at all for an issuer that is an origin.
The firewall¶
The application declares the user provider, the two firewalls of the provider and the authenticators on the second; the bundle declares none of it.
The session¶
The pages bind a browser to its session through the HTTP session of the framework. The standalone application names and bounds that cookie this way:
framework:
session:
name: '__Host-beffroi_session'
cookie_secure: true
cookie_httponly: true
cookie_samesite: lax
cookie_path: '/'
cookie_domain: ~
The __Host- prefix has the browser refuse the cookie unless it is Secure, its path / and its domain
unnamed, which the issuer of that application, an origin alone, allows. lax is the one value that cannot be
tightened: a client sends the browser to the authorization endpoint by a top-level navigation from its own
site, and strict would leave the cookie behind on it, so every authorization request would meet a provider
that does not know the visitor. What lax does to a post from another site is answered by the provider
itself.
What several processes share¶
A provider is several processes, the ones that serve requests and the worker, and some of what they hold has to be the same for all of them.
- The directory of the keys,
beffroi.keys.storage: the worker rotates the keys the other processes serve, so they read one directory. - The cache pools of the bundle, the keys published by clients and trusted issuers, the assertions, statements, request objects and DPoP proofs already seen, and the counts of the load budgets: a deployment of more than one machine points them at a store they share.
- The counts of the two limiters, kept in the cache pool of the rate limiters.
Messenger needs nothing more to start: every lane of the provider is the database unless the environment names a broker, and a worker consumes them.
The first run¶
bin/console doctrine:migrations:migrate
bin/console beffroi:tenant:synchronize
bin/console beffroi:keys:generate --algorithm=MESSAGE-SEAL
bin/console beffroi:keys:generate
- The schema. The migrations of the bundle are played with those of the application.
- The row of the tenant. The tenant is declared in the configuration and the command writes the row that says the same thing. Until it exists, a request fails rather than being served, and a command that works on the tenant stops and says to run this one first.
- The key messages are sealed to. Every message the provider writes to a transport is sealed to a key
of the tenant, and a key that is drawn announces itself on a transport: this key comes first, or the
next command stops on
Tenant "default" holds no MESSAGE-SEAL key: it is the first key a tenant is given, and nothing it writes to a transport leaves the process before it exists. - A signing key, of the default algorithm when none is named.
A key of any other algorithm the deployment serves is drawn the same way, under its name: VAPID for web
push, SAML-SIGNING for the identity provider of SAML. Then
comes a first person.
Checking the result¶
beffroi:doctorreads and writes nothing, says whether the deployment holds together point by point, and fails on a point it does not answer for, so a deployment pipeline can ask the same question.beffroi:keys:list --checkopens the keys in use, and the ones about to be, with the key management service, and fails on one it cannot open or on one that has worked past its age with nothing staged behind it.debug:routerlists the routes, those of the bundle under names that carry their plane.
The discovery document is the last check: <issuer>/.well-known/openid-configuration answers once the routes
are where the issuer says they are. Otherwise the first request that needs the endpoints fails with an
exception naming where the document is served and where the issuer makes clients look for it.
Changing the configuration later¶
What the configuration says of the tenant is copied to its row. After a change of its name, its look,
the claims its people may change or its acr vocabulary, beffroi:tenant:synchronize writes the row again;
until it does, a request fails with an exception naming the field that disagrees. A changed issuer is
refused: every token issued so far names the old one, and another issuer is another tenant, under another
slug.
A migration that seals columns says what to restart. A process that booted before it reads those columns with the type they had, so the processes that serve requests and the ones that consume the buses are restarted once it has run. Such a migration also refuses to run on a table that holds rows, naming the table and the count, since there is nothing to put in a sealed column from a value in clear.