The Request Edge
Everything the SDK enforces, it enforces here — in the middleware that runs before your controller, your entity set or your action handler sees the request. Four classes decide who may do what, and they run in a fixed order for a reason. This page is what you read when a request returns a 403 you did not expect, or a 200 you did.
Two stacks, not one
A LaravelUi5 host configures two middleware stacks in config/ui5.php. They are separate because they answer different questions: the ui5 stack asks may you open this artifact, the odata stack asks may you read this data.
'middleware' => [
'web',
\LaravelUi5\Core\Http\Middleware\ResolveUi5Context::class,
\LaravelUi5\Core\Http\Middleware\EnsureUi5Authenticated::class,
\LaravelUi5\Sdk\Http\Middleware\CheckAuthMiddleware::class,
],
'odata_middleware' => [
'web',
\LaravelUi5\Core\Http\Middleware\FetchCsrfToken::class,
\LaravelUi5\Core\Http\Middleware\ResolveODataEndpoint::class,
\LaravelUi5\Core\Http\Middleware\EnsureODataAuthenticated::class,
\LaravelUi5\Sdk\Http\Middleware\BindSdkContextForOData::class,
],Two of the four entries in each stack are Core's, one is the SDK's. That division is the whole architecture in miniature: Core resolves and authenticates, the SDK authorizes.
The ui5 stack, step by step
1 · ResolveUi5Context reads the request path, derives the artifact key from it, looks the artifact up in the registry, and binds a context. No artifact, no request — an unmatched path is a MissingArtifactException, not a 404 from your controller. Everything downstream can therefore assume $context->artifact() exists.
Under the SDK the bound context is an SdkContext, because the host points ui5.context_factory at the SDK's factory. That is what upgrades the context from "artifact + locale" to "artifact, tenant, actor, principal, locale, timestamp, abilities, settings".
2 · EnsureUi5Authenticated asks the artifact's module whether it requires authentication, and if so whether someone is signed in. Two escape hatches are worth knowing:
config('ui5.auth_enabled')—ENABLE_AUTH_4_UI5in.env— turns the whole gate off. It is a local-development convenience and nothing else.- A module that returns
falsefromrequiresAuth()is public by declaration.
An unauthenticated JSON request gets a 401 with a structured body; a browser request is redirected to the login route. Core owns no identity — it reads your guard and your route name.
3 · CheckAuthMiddleware is the SDK's gate, and it is three lines of logic:
$abilityId = $this->resolver->resolve($context->artifact());
if ($abilityId !== null && ! $context->abilities()->allowsAbilityId($abilityId)) {
abort(403);
}The artifact declares its own #[Access] ability; the resolver turns that into an id; the actor's resolved ability set either contains it or does not. An artifact with no #[Access] is open — the gate is default-open by design, so adding the SDK never silently closes a Core app.
Note the first line of the real implementation: if no SdkContext is bound at all, the middleware passes the request through. That is the requiresAuth = false path — nothing to check.
Why this is the only gate an app-scoped route needs
Any route that resolves to an app artifact is gated by that app's #[Access] before the controller runs. This is why the export endpoint has no ability check of its own, and why its controller would be dead code if it added one. When you add an app-scoped route, you inherit the gate — you do not re-implement it.
The odata stack, step by step
1 · FetchCsrfToken answers X-CSRF-Token: Fetch. The UI5 OData v4 model asks for a token before its first write and remembers what comes back; this middleware puts the current token in the response header so that handshake completes.
2 · ResolveODataEndpoint resolves the OData service for the request and binds it, so nothing downstream resolves it twice.
3 · EnsureODataAuthenticated is the OData counterpart of the UI5 auth gate.
4 · BindSdkContextForOData exists to close a gap, and the gap is worth understanding because its symptom is silent. The ui5 prefix binds the SdkContext as a side effect of ResolveUi5Context; the odata prefix has no equivalent step. Without this middleware, a request reaches a scoped entity set with no context bound, the scope applier cannot resolve an actor, and the scope falls through to 1 = 0.
The result is not an error. It is an empty list — the hardest failure mode there is to debug, because every layer reports success. If a scoped OData set returns nothing for a user who should see rows, check this middleware first.
It must sit after ResolveODataEndpoint (which binds the service it needs) and after EnsureODataAuthenticated (the context factory throws without an authenticated user).
The two gates OData has of its own
The middleware authenticates; two further gates live inside the read path and are documented with the Security domain:
#[Read]decides whether an actor may read an entity set at all. Default-open: a set without the attribute is unaffected. The SDK supplies the enforcer by rebinding the engine's authorizer interface — the engine itself knows nothing about abilities.- Scoping decides which rows. A separate question with a separate answer, and the one that depends on
BindSdkContextForODataabove.
An app's #[Access] does not protect its OData endpoint
The two prefixes are separate stacks with separate gates. Gating the app does not gate the data — that is what #[Read] is for. This is a deliberate 1.0 cut, and the read-gate page states it in full.
Where writes are checked
Writes do not ride either stack's authorization alone. Every write is an action on ui5/api/…, so it passes CheckAuthMiddleware for the app, and then its own #[Act] ability is checked by the form request before the handler runs. Two gates, deliberately: the app-open gate says you may be in the room, the #[Act] gate says you may press this button.
The SDK replaces Core's action dispatcher through a container rebind — no route surgery, since Laravel resolves route controllers through the container. A legacy Core handler keeps Core's path unchanged; a typed SdkActionHandlerInterface handler gets the SDK's path: validation gate, business context assembly, a dispatcher-owned database transaction, the seal check, and the wire envelope.
Adding your own middleware
Add it to the host's array in config/ui5.php — the whole stack is host-owned config, and artifact_resolvers alongside it. Two placement rules follow from the above:
- Anything that needs the artifact goes after
ResolveUi5Context. - Anything that needs the actor, the tenant or the ability set goes after
CheckAuthMiddlewareon theui5side, or afterBindSdkContextForODataon theodataside.
See Configuration for the complete file.