3
votes

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');
    }
1
Check Laravel logs, put it in dev mode, check your PHP logs.miken32
Have you tried different browsers? Also - have you tried to check that there is no firewall / .htaccess rule blocking access to the folder which the verification link directs to? Also - check what the httpd / apache logs say - especially the access logs - if there is an access violation, it will show up herealpharomeo
checked all logs, nothing i could see related to this issue. i have no idea what I would look for in .htaccess that I have not already checked cause as far as i can tell there is no "folder" to gain access to. the route in the email is just /email/verify/### and there is no route in my file for email/verify so it is one of those Laravelisms and I am not sure where that route goes but it is basically only executing code somewhere then going to my dashboard which I can navigate directly to so its not that. as I said all other pages in the site seem to work just fine so I am not sure where to lokWayne Fulcher
and I tried chrome, firefox, ie and edge, they all have same issue.Wayne Fulcher
Does your route use some middleware?Felippe Duarte

1 Answers

8
votes

If you have a Laravel app behind an apache proxy this also happens. In our case, we have more or less the same .env configuration and we also have

URL::forceScheme('https'); 

in our AppServiceProvider.

This creates the following urls: while signing the signature: https://..../email/verify/174?expires=1556027661 While verifying the signature: http://..../email/verify/174

our workaround is to replace the the 'signed' middleware: in app/Http/Kernel.php use 'signed' => \App\Http\Middleware\ValidateHttpsSignature::class, and then create this class with the following code:

namespace App\Http\Middleware;

use Closure;
use Illuminate\Routing\Exceptions\InvalidSignatureException;
use Illuminate\Http\Request;
use Illuminate\Support\Arr;
use Illuminate\Support\Facades\App;
use Illuminate\Support\Carbon;

class ValidateHttpsSignature
{
    var $keyResolver;

    public function __construct()
    {
        $this->keyResolver = function () {
            return App::make('config')->get('app.key');
        };
    }

    /**
     * gebaseerd op vendor/laravel/framework/src/Illuminate/Routing/Middleware/ValidateSignature.php
     * maar zorgt er voor dat een url altijd als https behandeld wordt. dit fixt het feit dat
     * laravel achter een rewrite proxy draait en urls binnenkrijgt als http.
     *
     * Handle an incoming request.
     *
     * @param  \Illuminate\Http\Request  $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  \Illuminate\Http\Request  $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);
    }

}