Livewire integration
Domain identification
Section titled “Domain identification”Add the following to your TenancyServiceProvider’s boot() method:
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:
'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.
Path identification
Section titled “Path identification”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:
- When you call
setUpdateRoute(), Livewire also adds a name to the passed route:livewire.update - If
setUpdateRoute()is not called, Livewire uses an existinglivewire.updateroute - When using path identification, we need a separate route
- 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:
'identification' => [ 'default_middleware' => Middleware\InitializeTenancyByPath::class,],'bootstrappers' => [ // ... Bootstrappers\UrlGeneratorBootstrapper::class,],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:
- As we’ve configured above, all Livewire routes are now universal.
- Universal routes on their own don’t work with path identification since the tenant routes
require the
{tenant}parameter and therefore must be separate. - 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.updateroute with the right identification middleware. Same for all the other routes.- You may notice that for the
FilePreviewControllerwe’re setting middleware on a controller not a route, however if you checkphp artisan route:list | grep preview-fileyou’ll see that the route got cloned just fine. This is because the middleware is set on the controller statically with theHasMiddlewareinterface, “bubbling up” into the actual route middleware even before invocation.
- You may notice that for the
- We enable the
UrlGeneratorBootstrapperand tell it to rewrite allroute('foo')calls toroute('tenant.foo')calls when in the tenant context. That way, theroute('livewire.update')call within Livewire will be changed toroute('tenant.livewire.update')and the'tenant'parameter will be passed usingURL::defaults() - 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):
Livewire::setUpdateRoute(function ($handle) { return RouteFacade::post('/livewire/update', $handle) ->middleware( 'web', 'clone', // no identification middleware );});
FilePreviewController::$middleware = [ 'web', 'clone', // no identification middleware];'temporary_file_upload' => [ // ... 'middleware' => [ 'throttle:60,1', 'clone', // no identification middleware ], // ...],'bootstrappers' => [ // ... Bootstrappers\UrlGeneratorBootstrapper::class,],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:
GET|HEAD {tenant}/livewire/preview-file/{filename} tenant.livewire.preview-file › Livewire\Features › FilePreviewController@handlePOST {tenant}/livewire/update ................ tenant.livewire.update › Livewire\Mechanisms › HandleRequests@handleUpdatePOST {tenant}/livewire/upload-file ........... tenant.livewire.upload-file › Livewire\Features › FileUploadController@handleWe 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.
Security testing
Section titled “Security testing”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-componenttenant1.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.