Jan 22, 2021 by Zacharia Mansouri | 20934 views
https://cylab.be/blog/122/using-https-over-a-reverse-proxy-in-laravel
Have you ever encountered problems trying to deploy a Laravel HTTPS website using a reverse proxy ? Then read the following to quickly learn where this problem comes from and how to simply thwart it.
If you use a proxy, here is how the website host server, the proxy server and the client machine probably communicate:
This means that whenever the client requests a page from the website, the page will be fetched from the proxy through HTTPS. That proxy will receive the HTTPS request from the client and then fetch the page from the website host through HTTP to send it back to the client through HTTPS. But before being sent, the page is assembled by Laravel (by the website host) that writes the location of the resources (JS scripts, CSS templates...) on that page using HTTP URLs, as asked by the proxy. When the page is retrieved by the client, it will try to retrieve those resources from their HTTP locations that will not be attainable because the proxy expects HTTPS requests from the client.
Here is however a very simple way to counter that.
Add before all the routes of routes/web.php:
$app_url = config("app.url");
if (app()->environment('prod') && !empty($app_url)) {
URL::forceRootUrl($app_url);
$schema = explode(':', $app_url)[0];
URL::forceScheme($schema);
}
// Your routes
This will retrieve the application URL from the .env file and force all the incoming requests to use HTTP or HTTPS depending on the APP_URL written in your .env file. Speaking of the devil, you must now precise in APP_URL which port you're using if you use a port different than the default ones (80 for HTTP and 443 for HTTPS). Concretely, here could be the value of APP_URL in your development .env file:
APP_URL=http://localhost:8000
You can see that the port 8000 is precised. And here could be the value of APP_URL in your production .env file:
APP_URL=https://my-https-website.org
Now everything should work as intended! But you might still want to read the next section...
If you use the Laravel UI authentication and got signature problems, know that they're also probably due to the use of HTTPS over a reverse proxy communicating with the website host using HTTP.
In fact, the first time I encountered the "HTTPS over a reverse proxy" problem was when generating the signature of the link written into the Laravel account validation e-mail. The server would always respond "403 Invalid Signature" when browsing that link. Here is why:
That error thus comes from the fact that the signature is generated with the HTTP URL and then verified with the HTTPS URL. Therefore Laravel considers the signature verification as failing and sends an error.
Here are the three steps to solve that error (based on this StackOverflow post and adapted to what we've done in the previous section):
Create a ValidHttpsSignature middleware:
php artisan make:middleware ValidHttpsSignature
and put the following as its content:
<?php
namespace AppHttpMiddleware;
use Closure;
use IlluminateRoutingExceptionsInvalidSignatureException;
use IlluminateHttpRequest;
use IlluminateSupportArr;
use IlluminateSupportFacadesApp;
use IlluminateSupportCarbon;
class ValidateHttpsSignature
{
var $keyResolver;
public function __construct()
{
$this->keyResolver = function () {
return App::make('config')->get('app.key');
};
}
/**
* Based in/laravel/framework/src/Illuminate/Routing/Middleware/ValidateSignature.php.
* Handle an incoming request.
*
* @param IlluminateHttpRequest $request
* @param Closure $next
* @return mixed
*/
public function handle($request, Closure $next)
{
if ($this->hasValidSignature($request)) {
return $next($request);
}
throw new InvalidSignatureException;
}
/**
* Determine if the given request has a valid signature.
* copied and modified from
* vendor/laravel/framework/src/Illuminate/Routing/UrlGenerator.php:363
* @param IlluminateHttpRequest $request
* @param bool $absolute
* @return bool
*/
public function hasValidSignature(Request $request, $absolute = true)
{
$url = $absolute ? $request->url() : '/'.$request->path();
// THE FIX:
$url = str_replace("http://","https://", $url);
$original = rtrim($url.'?'.Arr::query(
Arr::except($request->query(), 'signature')
), '?');
$expires = $request->query('expires');
$signature = hash_hmac('sha256', $original, call_user_func($this->keyResolver));
return hash_equals($signature, (string) $request->query('signature', '')) &&
! ($expires && Carbon::now()->getTimestamp() > $expires);
}
}
'signedhttps' => AppHttpMiddlewareValidateHttpsSignature::class,
below the following line:
'signed' => IlluminateRoutingMiddlewareValidateSignature::class,
$this->middleware('signed')->only('verify');
by:
$app_url = config("app.url");
if (!empty($app_url)) {
$schema = explode(':', $app_url)[0];
if ($schema == 'https') {
$this->middleware('signedhttps')->only('verify');
} else {
$this->middleware('signed')->only('verify');
}
}
And you're done! That was easy, wasn't it?
This blog post is licensed under CC BY-SA 4.0