Signed route for email verification does not pass signature validation

老子叫甜甜 提交于 2020-01-11 10:23:28

问题


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:


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);
    }

}


来源:https://stackoverflow.com/questions/52525958/signed-route-for-email-verification-does-not-pass-signature-validation

易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!