Skip to content

Livewire integration

Add the following to your TenancyServiceProvider’s boot() method:

app/Providers/TenancyServiceProvider.php
Livewire::setUpdateRoute(function ($handle) {
return RouteFacade::post('/livewire/update', $handle)
->middleware(
'web',
'universal',
tenancy()->defaultMiddleware(),
);
});
FilePreviewController::$middleware = [
'web',
'universal',
tenancy()->defaultMiddleware(),
];

And update the file upload route middleware:

config/livewire.php
'temporary_file_upload' => [
// ...
'middleware' => [
'throttle:60,1',
'universal',
tenancy()->defaultMiddleware(),
],
// ...
],

You could also use a specific tenancy middleware here, instead of the defaultMiddleware() call, such as InitializeTenancyByDomain. The defaultMiddleware() method returns the value of the tenancy.identification.default_middleware config.

To integrate Livewire when using path identification, the process is slightly more complicated.

First, here’s a bit more context about how setUpdateRoute() actually works:

  1. When you call setUpdateRoute(), Livewire also adds a name to the passed route: livewire.update
  2. If setUpdateRoute() is not called, Livewire uses an existing livewire.update route
  3. When using path identification, we need a separate route
  4. We need to somehow tell Livewire to use the separate tenant route when in the tenant context

To solve point 3), we can use route cloning. To solve point 4), we can use the UrlGenerator bootstrapper.

If you followed the steps above, you can make the following changes to make Livewire work with path identification:

config/tenancy.php
'identification' => [
'default_middleware' => Middleware\InitializeTenancyByPath::class,
],
'bootstrappers' => [
// ...
Bootstrappers\UrlGeneratorBootstrapper::class,
],
app/Providers/TenancyServiceProvider.php
public function boot()
{
TenancyUrlGenerator::$prefixRouteNames = true;
}
protected function mapRoutes()
{
// ...
// $this->cloneRoutes();
$this->cloneRoutes();
}
protected function cloneRoutes(): void
{
/** @var CloneRoutesAsTenant $cloneRoutes */
$cloneRoutes = $this->app->make(CloneRoutesAsTenant::class);
/** See CloneRoutesAsTenant for usage details. */
$cloneRoutes->cloneRoutesWithMiddleware(['universal'])->handle();
}

Here’s what’s happening with this setup:

  1. As we’ve configured above, all Livewire routes are now universal.
  2. Universal routes on their own don’t work with path identification since the tenant routes require the {tenant} parameter and therefore must be separate.
  3. We use the route cloning feature and tell it to clone all universal routes, thereby creating the tenant versions of those universal routes. We now have a tenant.livewire.update route with the right identification middleware. Same for all the other routes.
    • You may notice that for the FilePreviewController we’re setting middleware on a controller not a route, however if you check php artisan route:list | grep preview-file you’ll see that the route got cloned just fine. This is because the middleware is set on the controller statically with the HasMiddleware interface, “bubbling up” into the actual route middleware even before invocation.
  4. We enable the UrlGeneratorBootstrapper and tell it to rewrite all route('foo') calls to route('tenant.foo') calls when in the tenant context. That way, the route('livewire.update') call within Livewire will be changed to route('tenant.livewire.update') and the 'tenant' parameter will be passed using URL::defaults()
  5. The central routes are ostensibly universal but in practice they’re fully central and the path identification middleware doesn’t do anything as there is no {tenant} parameter on those routes.

This is the easiest way to translate the changes we’ve made for domain identification to a path identification setup, however, it’s fairly messy. If you’d prefer a cleaner setup, we can do this instead (from scratch, without the existing changes):

app/Providers/TenancyServiceProvider.php
Livewire::setUpdateRoute(function ($handle) {
return RouteFacade::post('/livewire/update', $handle)
->middleware(
'web',
'clone',
// no identification middleware
);
});
FilePreviewController::$middleware = [
'web',
'clone',
// no identification middleware
];
config/livewire.php
'temporary_file_upload' => [
// ...
'middleware' => [
'throttle:60,1',
'clone',
// no identification middleware
],
// ...
],
config/tenancy.php
'bootstrappers' => [
// ...
Bootstrappers\UrlGeneratorBootstrapper::class,
],
app/Providers/TenancyServiceProvider.php
public function boot()
{
TenancyUrlGenerator::$prefixRouteNames = true;
}
protected function mapRoutes()
{
// ...
// $this->cloneRoutes();
$this->cloneRoutes();
}
protected function cloneRoutes(): void
{
/** @var CloneRoutesAsTenant $cloneRoutes */
$cloneRoutes = $this->app->make(CloneRoutesAsTenant::class);
/** See CloneRoutesAsTenant for usage details. */
// The action defaults to cloning routes with the 'clone' middleware
$cloneRoutes
->addTenantMiddleware(Middleware\InitializeTenancyByPath::class)
->handle();
}

With this setup, the central routes don’t have a meaningless universal middleware flag and an unused path identification middleware. We also don’t trigger cloning for all universal routes as that may be affecting other routes. We only mark these three routes with clone and then follow a similar process to the one described above.

You can confirm that all three Livewire routes got cloned using route:list:

php artisan route:list | grep tenant.livewire
GET|HEAD {tenant}/livewire/preview-file/{filename} tenant.livewire.preview-file Livewire\Features FilePreviewController@handle
POST {tenant}/livewire/update ................ tenant.livewire.update Livewire\Mechanisms HandleRequests@handleUpdate
POST {tenant}/livewire/upload-file ........... tenant.livewire.upload-file Livewire\Features FileUploadController@handle

We intentionally covered both approaches so you can see two things that are fairly common when integrating third-party packages with path identification: Tenancy has a lot of features that can make path identification feel close to domain identification, as far as integration code goes. However, there’s often a cleaner way to do things by working specifically with the fact that path identification is your chosen identification approach.

Livewire is inherently an attack vector, being a full-stack framework with a significant backend attack surface. Multi-tenant applications often require more care as the tenant separation logic is often integrated into existing code, as above, and there’s often a lot of harm that could come from improperly mixing tenant data.

Out of the box, you shouldn’t face any security issues. However if you’re performing a security audit, the following steps may be helpful.

Imagine you’re using the same component, on the same path, in the tenant and central context:

  • acme.com/my-component
  • tenant1.acme.com/my-component

The component injects User::first() in mount() and lets the user make some changes to it.

When a user performs a change that calls the backend, Livewire makes a request to /livewire/update — the update route we’ve discussed above — and passes the component information along with the user’s changes.

Now consider this: if you have the same component that injects the same model (say you’re using App\Model\User in both contexts — generally not recommended but not an issue on its own) what happens if you hijack the Livewire frontend to send the request to the wrong domain? Instead of tenant1.acme.com, make the request to acme.com — or vice versa, or yet another tenant domain.

To test this, you can trigger an action in the component while having Chrome DevTools open. Go to the Network tab, see the update request. Click on the initiator and place a breakpoint on that line of JavaScript, it should include a fetch() call. Trigger an action again and the breakpoint should trigger. In the Console tab, you should now be able to access updateUri. It should be (in domain identification setups) /livewire/update. Change it to the other context, e.g. http://acme.com/my-component if you’re in the tenant context currently (replace with the right protocol and domain in your own test).

You should see a CORS error, assuming well-configured CORS. That’s the first line of defense here.

If you don’t see a CORS error, due to more permissive CORS headers, you should still see a 419 response because Livewire’s internals detected tampering.

If you try to do the same test, but with path identification, you should only see the 419 response since there’s no CORS involved on a single domain.

The point of this section isn’t to stress you out about security — as mentioned in the paragraphs above, there are several protection mechanisms at play out of the box. However, when using tools like Livewire, especially in a multi-tenant context, it is helpful to know how those tools work under the hood, what protection mechanisms they have against user tampering — specifically against trying to change one tenant to another, or switch between the tenant and central context. This should be helpful during a security audit, if you just want to confirm for yourself that Livewire’s security mechanisms are actually protecting you here, or if you just want to understand how tenant separation is achieved in this context, given how Livewire works.