Heroku, Laravel, Load Balancers & SSL

Recently I've been working with Heroku for a few Laravel projects and I came across an issue with how assets were being loaded over non-ssl based urls.

Background

Heroku's HTTP Routing routes each request through a layer of reverse proxies which are, among other things, responsible for load balancing and terminating SSL connections.

The Problem

This means that deep down in Symfony core (The building blocks of Laravel), the HTTP Request class checks for the HTTPS server variable when checking if the request is secure ...

public function isSecure()
{

    ...

    $https = $this->server->get('HTTPS');

    ...

}

... and because the SSL connection is terminated at the load balancer, this variable is set to false.

Middleware to the Rescue

The solution was to create a Middleware which will trust the Heroku load balancer as a proxy, and configure some additonal headers to maximise security. The exact details have been taken from the Heroku Symfony example within their docs and modified to work with Laravel's Middleware:

Because I only wished for this middleware to be used on certian environments, i.e. staging and production, I've added the following check into the code to ensure the environment is correct before applying the settings:

public function handle(Request $request, Closure $next)
{

    if (app()->environment(['staging', 'production'])) {

        // Apply Settings Here

    }

    return $next($request);

}

The above example has been simplified. The actual code utilises dependency injection to aid in testability.

Complete Solution

Combining the above environment checking, and the specific settings required by Heroku, results in the following complete middleware:

namespace App\Http\Middleware;

use Closure;
use Illuminate\Contracts\Foundation\Application;
use Illuminate\Http\Request;

class TrustHerokuLoadBalancer
{
    /**
     * @var  Application
     */
    private $app;

    /**
     * Environments to trust the proxies on.
     *
     * @var  array
     */
    private $envs = [
        'staging',
        'production',
    ];

    /**
     * @param  Application $app
     */
    public function __construct(
        Application $app
    ) {
        $this->app = $app;
    }

    /**
     * Handle an incoming request.
     *
     * @param    Request $request
     * @param    Closure $next
     *
     * @return  mixed
     */
    public function handle(Request $request, Closure $next)
    {

        if ($this->app->environment($this->envs)) {

            $request->setTrustedHeaderName(Request::HEADER_FORWARDED, null);
            $request->setTrustedHeaderName(Request::HEADER_CLIENT_HOST, null);

            $request->setTrustedProxies([
                $request->getClientIp(),
            ]);

        }

        return $next($request);

    }
}

Finally, to ensure this middleware is run on each load of a page within the site, it should be added to the App\Http\Kernel class, specifically the protected $middleware array:

...

class Kernel extends HttpKernel
{

    ...

    protected $middleware = [
        \App\Http\Middleware\TrustHerokuLoadBalancer::class,
        \Illuminate\Foundation\Http\Middleware\CheckForMaintenanceMode::class,
    ];

    ...

}

That should be it. Providing your APP_ENV matched one of the environments listed in the environments-to-trust array, it should function correctly.

The actual implementation of this can be found here.