External actions & inline hooks
External actions & inline hooks
Inline hooks (Cbox\Id\ExternalActions\) are synchronous extension points: at a
named point in a flow, the platform pauses and consults registered logic that can
enrich the operation (add data) or veto it (deny) — an inline-hook capability:
your code runs inside the auth pipeline, not after it.
It is deliberately different from webhooks: a webhook notifies asynchronously and cannot change the outcome; a hook participates in-band and can.
Hook points
Six, spanning the token, login, registration and credential flows. Each one names when it fires, whether a deny stops anything, and what an unreachable hook means there:
| Hook point | Fires | Vetoes | Unreachable |
|---|---|---|---|
token_minting |
before an access token is signed, on every grant | yes | denies |
post_login |
after authentication, before the session row | yes | allows |
pre_registration |
before a subject is created | yes | denies |
post_registration |
just after, with the new subject id | no | allows |
pre_password_change |
before a credential is written | yes | denies |
post_password_change |
just after | no | allows |
Full payload shapes, fail policies and worked examples: Hook points.
Two kinds of action
In-process (dependency-light) — a class implementing Contracts\Action, listed in
config. Deny-by-default: only listed classes run.
final class AddTenantTier implements Action
{
public function handle(ActionContext $context): ActionResult
{
$tier = /* look up the org's plan */;
return ActionResult::continue(['tenant_tier' => $tier]); // add a claim
// or: return ActionResult::deny('org is over quota'); // veto
}
}
// config/cbox-id.php
'external_actions' => ['hooks' => ['token_minting' => [AddTenantTier::class]]],
External HTTP — register a customer HTTPS endpoint; the platform calls it
synchronously and interprets the JSON reply. The request is SSRF-guarded and HMAC-signed
(the same scheme as webhooks); the reply is
{"action":"continue"|"deny","claims":{…},"reason":"…"}.
$registered = app(ExternalActions::class)->register(HookPoint::TokenMinting, 'https://hooks.acme.com/token');
// $registered->secret — the reveal-once HMAC secret the endpoint verifies X-Cbox-Signature with.
A hook point runs its in-process actions first, then its external endpoints, folding the results: the first deny short-circuits; enrichment is merged (later wins).
Fail policy: closed at the gates, open on the login path
If a hook can't be consulted — an in-process action throws, or an external endpoint
times out / errors / returns non-2xx — what happens is that hook point's decision. Every
gate (token_minting, pre_registration, pre_password_change) denies: a security
control that fails open is not a control. post_login allows, because failing closed
on the hottest path in the product hands one customer-controlled URL the power to lock a
whole tenant out of everything, admin console included; the notify-only points allow
because they have no decision to fail closed to.
A hook that is consulted and denies always denies. That is never configurable.
Override per point with external_actions.fail_policy.<hook> ('open' / 'closed'), or
for all of them with external_actions.fail_open (a bool; leave it unset to keep the
per-point defaults).
Honest scope
- There is no separate
credentials_exchangepoint.token_mintingalready fires on theclient_credentialsgrant; its payload'sgrantfield is how you filter for it. - The external call is on the auth hot path. Keep the endpoint fast — the timeout is short (default 3s) and there is no retry (a hook is synchronous, not a webhook). A fan-out of several endpoints goes out concurrently, so the whole pipeline costs one endpoint's timeout rather than one per endpoint.
- Registration and password-change hooks are environment-scoped in practice. Neither
operation carries an organization, so only environment-level endpoints
(
organization_idnull) fire for them.token_mintingandpost_logindo carry one, and a tenant's own endpoints fire for it. - A hook cannot rewrite protocol claims.
iss,sub,exp,scope,aud,cnf,ent, … are protected; enrichment only adds non-reserved keys. - This is a primitive, not a policy. What a hook decides is the host's logic; the platform guarantees the mechanics — deny-by-default, fail-closed, signed, SSRF-guarded, audited.
Where to go next
- Hook points — every point, payload and fail policy.
- Add a token claims hook — the recipe.
- Custom hook action — the contract in detail.
- Security: external actions — the threat model.