I recently updated my project from Laravel 5.6 to 5.7 and added the email verification steps described by Laravel docs to my project. Everything works great on my development machine (which is http) but when I update my production server (which is https) with all changes then when laravel sends me the email with the link (signed route) it generated for me to click button or paste into my browser laravel seems to not be able to validate the signature it created. The side effect is every time I click the button or paste the link into the browser I get the error:
403 Sorry, you are not authorized to access this page.
What I have traced down so far is I found the code in laravel's ValidateSignature.php class and I added some log messages.
public function handle($request, Closure $next)
{
Log::info('checking signature');
if ($request->hasValidSignature()) {
Log::info('signature is valid');
return $next($request);
}
Log::info('throwing InvalidSignatureException');
throw new InvalidSignatureException;
}
And more specifically I traced the exact issue inside the laravel unit UrlGenerator.php I added the Logs in the following method:
public function hasValidSignature(Request $request)
{
$original = rtrim($request->url().'?'.Arr::query(
Arr::except($request->query(), 'signature')
), '?');
$expires = Arr::get($request->query(), 'expires');
$signature = hash_hmac('sha256', $original, call_user_func($this->keyResolver));
Log::info('url: '.$original);
Log::info('expire: '.$expires);
Log::info(' new signature: '.$signature);
Log::info('link signature: '.$request->query('signature', ''));
Log::info('hash equals: '.hash_equals($signature, $request->query('signature', '')));
Log::info('expired: '.!($expires && Carbon::now()->getTimestamp() > $expires));
return hash_equals($signature, $request->query('signature', '')) &&
! ($expires && Carbon::now()->getTimestamp() > $expires);
}
When i click button or paste link in browser and press enter I get the following log messages: (I changed my real domain for obvious reasons.... not try to market my site or something)
checking signature
url: http://www.example.com/email/verify/2?expires=1538012234
expire: 1538012234
new signature: 1326b9e7402a51e0f05ddf1cb14f1e14852b4c5f0d1d6e726554806e7d85b4b1
link signature: e1d3ad5dc88faa8d8b0e6890ef60e216b75d26ef7ed5c6ab1cc661548e0ad8df
hash equals:
expired: 1
throwing InvalidSignatureException
So I don't know if the bug is in the logic where laravel creates initial signature or when it is trying to validate it. However like I said it all works great on my development machine. I have cleared cache, cleared routes, updated to latest code, rebooted server, everything I can think of. Any help would be greatly appreciated.
**** UPDATE *****
I dug a little deeper and have narrowed down the problem. I can't believe I didn't see this last night. If we look closely at the output logs listed above the one log message
url: http://www.example.com/email/verify/2?expires=1538012234
shows us the problem. So as I said before my development machine is http but my live server is https. I see this morning (after a good 4 hours sleep) that the log shows us that somehow the logic in the method hasValidSignature() is getting a route with http instead of https. So when I go back to my email the link in the email is https, if I paste the url in my browser it has https, and in my browser after this logic returns the 403 error the browser still shows https. So now we can focus on how does my route/url get converted to http? I am really struggling here cause I have no idea how that url is processed anyhow since /email/verify is not even listed in any of my routes files (that I know of) and I can't say I understand what to look for under the hood for this either so I am really hoping for some help here.
Also here are the settings in my .env file:
APP_USE_HTTPS=true
APP_URL=https://www.example.com
APP_ENV=production
And in the boot method of the AppServiceProvider I have
public function boot()
{
Schema::defaultStringLength(191);
if (env('APP_USE_HTTPS'))
{
Log::info('forcing URLs to use https');
\URL::forceScheme('https');
}