AxiamAuthSubscriber
in package
implements
EventSubscriberInterface
Symfony authentication subscriber (D-02, CONTRACT.md §10): listens to `kernel.request`, extracts the bearer/cookie token, verifies it via {@see AxiamClient::verifyLocally()} — the no-fallback seam mandated by §10.1 rule 8 — and populates the `axiam_user` request attribute with `user_id`/`tenant_id`/`roles` on success. Short-circuits the request with a standardized 401 JSON error body on any failure (missing token, invalid signature, expired token). Never duplicates JWKS-verify logic itself (D-02 prohibition) — every security-critical decision is made by {@see AxiamClient}.
The decision is always about the CALLER's credential. This subscriber deliberately does not use AxiamClient::verifyLocallyOrFallback(): its reactive-refresh fallback verifies this application's own session, so a request whose token fails verification would be admitted under the app's own principal (SEC-085).
MUST be manually registered (Pitfall 5): Symfony has no Laravel-style zero-config
auto-discovery for a plain composer require without a published Flex recipe (out
of scope this phase). A consuming app must tag this class
kernel.event_subscriber in its own config/services.yaml in addition to listing
AxiamBundle in config/bundles.php — see examples/symfony_app/.
CSRF (cookie double-submit, CONTRACT.md §3): when the credential was sourced from
the axiam_access COOKIE (not the Authorization header) and the request method
is state-changing (anything other than GET/HEAD/OPTIONS), this subscriber
additionally requires the X-CSRF-Token request header to be present and equal
(constant-time) to the axiam_csrf cookie value, short-circuiting with 403 on
mismatch/absence. Bearer-header requests are CSRF-immune by construction — a
cross-site attacker cannot set arbitrary request headers — but a cookie
automatically attached by the browser is not, and in any same-site deployment
where axiam_access reaches this app, the non-httpOnly axiam_csrf cookie does
too. This mirrors, locally, the same double-submit check the AXIAM server performs
on its own endpoints (§3).
MCP resource-server helpers (CONTRACT.md §28, opt-in): supplying $resourceMetadataUrl
turns on the RFC 6750 WWW-Authenticate challenge on every 401 this subscriber
emits, and exempts the metadata document's own path from authentication — see
McpGuardOptions for the precomputation and
ProtectedResourceMetadataController for serving the document itself. With
$resourceMetadataUrl unset (the default), this subscriber behaves byte-for-byte
as it did before §28 existed — no header on any response, no status changed, no
body changed, no path exempted.
Table of Contents
Interfaces
- EventSubscriberInterface
Methods
- __construct() : mixed
- client() : AxiamClient
- The `AxiamClient` this subscriber verifies against — the same accessor {@see \Axiam\Sdk\Mcp\Mcp::checkGuardAgreement()} reads `expectedAudience()` from for the §28.5 rule 3 cross-check.
- getSubscribedEvents() : array<string, string>
- mcpGuardOptions() : McpGuardOptions|null
- This subscriber's own CONTRACT.md §28.5 configuration, or `null` when `$resourceMetadataUrl` was not supplied at construction.
- onKernelRequest() : void
- Authenticates the inbound request and, for cookie-authenticated writes, enforces CSRF.
Methods
__construct()
public
__construct(AxiamClient $client, string $tenant[, string|null $resourceMetadataUrl = null ]) : mixed
Parameters
- $client : AxiamClient
-
Client used to verify the presented token against the cached JWKS.
- $tenant : string
-
Tenant slug the verified token's claim must match.
- $resourceMetadataUrl : string|null = null
-
CONTRACT.md §28.5's opt-in option: the URL of this resource server's RFC 9728 metadata document (the
metadataUrla priorMcp::protectedResourceMetadata(...)call returned).null(the default) leaves this subscriber's behaviour byte-for-byte unchanged from before §28 existed. Setting it REQUIRES$clientto have been constructed withexpectedAudience— refused at construction, naming both options, when it was not (§28.5 rule 2).
client()
The `AxiamClient` this subscriber verifies against — the same accessor {@see \Axiam\Sdk\Mcp\Mcp::checkGuardAgreement()} reads `expectedAudience()` from for the §28.5 rule 3 cross-check.
public
client() : AxiamClient
Return values
AxiamClientgetSubscribedEvents()
public
static getSubscribedEvents() : array<string, string>
Return values
array<string, string>mcpGuardOptions()
This subscriber's own CONTRACT.md §28.5 configuration, or `null` when `$resourceMetadataUrl` was not supplied at construction.
public
mcpGuardOptions() : McpGuardOptions|null
Public so ProtectedResourceMetadataController can cross-check that the document it is about to publish agrees with THIS subscriber's own configuration (§28.5 rule 3) when both are wired together — never required, since the two may legitimately be configured in separate processes.
Return values
McpGuardOptions|nullonKernelRequest()
Authenticates the inbound request and, for cookie-authenticated writes, enforces CSRF.
public
onKernelRequest(RequestEvent $event) : void
Sequence: extract the credential (Authorization: Bearer first, then the
axiam_access cookie) → verify the JWT locally against the cached JWKS → enforce the
cross-tenant claim check → inject the identity onto the request.
CSRF (CONTRACT.md §3a): when the credential came from the COOKIE and the method is
state-changing (not GET/HEAD/OPTIONS), the X-CSRF-Token header must be present and
equal (constant-time) to the axiam_csrf cookie, else a 403 response is set on the
event. Bearer-authenticated requests are exempt — a cross-site attacker cannot set
custom request headers.
Parameters
- $event : RequestEvent
-
Kernel request event; a 401/403 JSON response is set on it to short-circuit the request on failure.