We use a Laravel-powered dashboard app at Highland to monitor the health of the company. The app updates all its data using scheduled tasks, so aside from consuming that data, we don’t interact with the app. We’ve no need for user accounts except to keep the info private, but creating accounts and logging in feels excessive, so we decided to lock things down another way.
Our first solution was to set up a firewall, but when team members started building Slack apps and Alexa skills that query the dashboard’s API, we knew we needed to manage whitelisted IPs at a higher-level to more easily account for volatile IPs. We opted for a fairly simple solution powered by middleware and Trusted Proxies (a package Laravel includes by default) for the front end and, of course, Passport for the API.
Implementing the solution
If you clicked the above link to Laravel’s docs, you’ll notice they tell you to just drop your proxies in app/Http/Middleware/TrustProxies.php all willy nilly, but that doesn’t always play out so well across different environments. Inspecting the extended Fideloper\Proxy\TrustProxies class for an unintrusive alternative, I discovered it pulls from the config in its method setTrustedProxyIpAddresses() which the oh-so-familiar handle() method (or, dare I say, gentleman-caller) calls for us.
I could just drop a new config variable in config/app.php and use it in app/Http/Middleware/TrustProxies.php, but Trusted Proxies already has one, so it feels a little more out-of-the-box to just use it.
I only need to run php artisan vendor:publish —-provider="Fideloper\Proxy\TrustedProxyServiceProvider" so I can drop an environment variable in there.
Side note: I name my environment variables after the related file and config key, e.g. TRUSTEDPROXY_PROXIES for config/trustedproxy.php's proxies key. This standard prevents naming collisions and keeps teams from getting frustrated over Jimmy’s HORRIBLE NAMING CHOICES, which in turn saves time during retros. #protip
I ran into some fun⸮ issues and discovered that specifying my proxies as an array works on the server, but using * (WHITELIST ALL THE IPS! _o/) in an array was problematic when running tests on Travis. That explains the unsightly string vs. array war at the top of my config/trustedproxy.php.
Trusted Proxies doesn’t actually block IPs. It just happens to overlap cleanly enough for my taste to repurpose things a bit. To block any IPs not whitelisted as trusted proxies, we need some simple middleware. So simple I won’t even explain it. Here it is:
When registering the middleware, I didn’t want to wrap everything in routes/web.php in a single Route::group(), so I just added it to the web middleware group in app/Http/Kernel.php to keep from muddying things up.
Potentially relevant to some… our dashboard’s front end is an SPA that uses API-querying Vue components, and worrying about bearer tokens when dogfooding an API on the same server is unnecessarily tedious nonsense. So I haz another middleware for you to point your lookyballs at. It suffices for our uses, but definitely not all, so don’t use it blindly.
This checks if the request is from one of the whitelisted IPs and injects our bearer token if so. When getting the bearer token, it pulls from the cache if already set, otherwise it deletes the existing token(s) (just to keep the database a bit more manageable) then creates and caches a new one. Of course, I also need to assign the middleware a key in app/Http/Kernel.php’s $routeMiddleware. I choose you, Pika-injectbearertoken-chu!
I also need to set the middleware priority to ensure the app calls the InjectBearerTokenForLocalApiRequests middleware early enough that the app doesn’t reject the requests. The App\Http\Kernel class extends Illuminate\Foundation\Http\Kernel which already has the $middlewarePriority property defined. I copied that over to app/Http/Kernel.php and dropped my middleware in there as late in priority as practical.
Since not all of the dashboard’s API endpoints are relevant, I opted to Route::group() the relevant jabronis in routes/api.php.
With this in place:
- we can remove the firewall,
- my team members can query the API (with Passport securing that side), and
- Bob’s your uncle (assuming you have an Uncle Bob… I do not).
Let me know if you have any interest in seeing some tests or would benefit from a writeup on working with Projector’s API, which, for the uninitiated, is a bit less straightforward than we typically expect from modern apps.