Why an event bridge
A grant is issued against the facts at the time: the business was active, the case was open, the principal was employed. Facts change after issuance. A provider that monitors a business learns it was dissolved; an identity provider learns a session was revoked. The event bridge lets those systems tell the auth service, so the grants that relied on the old facts stop working. The bridge has three rules:- Only verified events count. A delivery that does not verify is refused
with
401, counted, and never acted on. - An event is acted on at most once. Every event id is claimed in a replay store before anything happens.
- Nothing is inferred. A verified event that no mapping rule matches is logged, counted and ignored. It never widens or narrows a grant by default.
EVENT_BRIDGE_ENABLED=true. With it off every event
bridge route answers 404 (ingestion) or 403 FEATURE_DISABLED
(registration), and nothing else in the auth service changes.
Registering a source
Sources belong to one developer and are managed with the developer API key.SSF/CAEP transmitter
A transmitter pushes Security Event Tokens (RFC 8417) to the receiver, as in RFC 8935 push delivery. Register its issuer and its public keys, inline or by URL:ingestUrl (/v1/event-bridge/ssf/<id>). audience
defaults to that URL; set it to what the transmitter puts in aud if it
differs. jwksUri is fetched through the same outbound guard as webhook
URLs (no private hosts in production), cached for five minutes and
refetched at most every 30 seconds when a token names an unknown kid.
Inline jwks must contain public keys only.
A SET is accepted only when all of these hold, and each failure has its own
reason code:
The subject is read from the SSF
sub_id claim, or from an event’s subject
member for transmitters on earlier CAEP drafts.
Generic signed webhook
For systems that do not speak SSF, register a webhook source:secret once. It is stored encrypted with
VAULT_ENCRYPTION_KEY, bound to the source id. The sender signs the
timestamp and the exact body bytes:
X-Grantex-Signature-V2), so one signer serves both directions. The body is
JSON:
toleranceSeconds of the receiver’s clock, in either direction
(timestamp_out_of_window). Several sha256= values may be sent, comma
separated, while the sender changes secrets.
Rotation. POST /v1/event-sources/<id>/rotate-secret returns a new
secret. The previous one keeps verifying for previousSecretTtlSeconds
(default one day, at most seven). Pass 0 for a leaked secret so it stops
immediately.
Replay protection
Each verified delivery claims(source, event id) — the SET jti or the
webhook id — before it is processed:
A replay outside the webhook window, or of a SET older than
maxAgeSeconds,
is refused before it reaches the replay store.
Responses
202 {"status": "unmapped" | "applied" | "observed" | "duplicate"}401 {"err": "<reason>", "description": "…", "code": "EVENT_UNVERIFIABLE"}for every verification failure, including an unknown or disabled source (so source ids cannot be probed)415for the wrong media type404when the bridge is off for the source’s developer5xxwhen processing failed; the receipt is leftfailedso the sender’s retry is processed again
Observability
Every refused delivery also logs
alert: "event_bridge_verification_failure"
with the source id and reason, never the payload or signature. Alert rules are
in deploy/prometheus/event-bridge-alerts.yml.
Settings
What this does not defend against
- A transmitter whose signing key is stolen can send events that verify.
Scope what its events can do with mapping rules, and disable the source
(
PATCH /v1/event-sources/<id>with{"status": "disabled"}) if its key leaks. - Events are only as timely as the sender. The bridge bounds how old an accepted event may be, not how late the sender is.
- A disabled source’s events are refused, not queued: re-enable it and have the sender retransmit.