4
votes

I'm trying to take advantage of the new signed middleware in Laravel 5.7, but for some reason the generated signed URL is returning 403 Invalid Signature.

I'm using the latest Laravel Version, with PHP 7.2

This is my web.php route:

Route::get('/report/{user}/{client}', function ($user, $client) {
    return ("El usuario es: $user y el cliente es: $client");
})->name('report.client')->middleware('signed');

and this is in my controller:

$objDemo->tempURL = Url::temporarySignedRoute('report.client', now('America/Panama')->addDays(5), [
            'user' => 1,
            'client' => 1
        ]);

The URL is generated and shows something like this:

https://example.com/report/1/1?expires=1545440368&signature=55ad67fa049a74fe8e123c664e50f53564b76154e2dd805c5927125f63c390a1

But when i click the link the result is a 403 with the message: "Invalid signature"

Any ideas? thanks in advance

-----------UPDATE------------

Things i've done already:

  1. Try the route without signing, and works perfectly
  2. Try the route without parameters and only signing
  3. Try the route without temporary setting and only signing
  4. Set cloudflare's ip to trusted proxies
  5. Disable HTTPS, Enable HTTPS

Nothing seems to work, always getting the 403 invalid signature page

-----------UPDATE 2------------

Ok, so after some digging and testing, i found out that laravel signed routes won't work if the user is logged in, this is weird, if i logout then the route works perfectly, but if i log-in then it shows the 403 error, might this be because Laravel adds the session cookie header after everything else? and so the signed route fails because of it? it's this the way it should be?

Weird, because let's say i want to create a temporary link for my users to download something, if they are logged into my Laravel app, they will get this 403 error message... :(

------------UPDATE 3------------------

I tried in a fresh installation of laravel and worked perfectly, so it's something from my main Laravel app, also tried to install every composer dependency into the Fresh installation of Laravel, and still worked perfectly no matter the user login status, so it's not a conflict with my dependencies.

9
@LaraDev you digging absolutely on right way.Vinay Kaithwas

9 Answers

9
votes

After debugging UrlGenerator::hasValidSignature(), i ended by DD the variables inside UrlGenerator.php like this:

public function hasValidSignature(Request $request, $absolute = true)
    {
        $url = $absolute ? $request->url() : '/'.$request->path();

        //dd($url);

        $original = rtrim($url.'?'.Arr::query(
            Arr::except($request->query(), 'signature')
        ), '?');

        dd($original);
        $expires = Arr::get($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);
    }

the $original variable showed me what was actually happening with my URL, and showed this:

https://example.com/report/1/1?expires=1546586977&settings%5Bincrementing%5D=1&settings%5Bexists%5D=1&settings%5BwasRecentlyCreated%5D=0&settings%5Btimestamps%5D=1&profile%5Bincrementing%5D=1&profile%5Bexists%5D=1&profile%5BwasRecentlyCreated%5D=0&profile%5Btimestamps%5D=1&user%5Bincrementing%5D=1&user%5Bexists%5D=1&user%5BwasRecentlyCreated%5D=0&user%5Btimestamps%5D=1

as you can see there are parameters after the expires parameter, those parameter where aded after the route creation, and that was the problem, this happened because i had a middleware sharing some information to the views like this:

UserDataMiddleware.php

<?php

namespace App\Http\Middleware;

use Closure;
use Illuminate\Support\Facades\Auth;
use App\User;
use App\Setting;
use App\UserProfile;
use Illuminate\Support\Facades\View;

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

        if (Auth::check()) {
            $settings = Setting::where('user_id', Auth::user()->id)->first();
            $profile = UserProfile::where('user_id', Auth::id())->first();
            $user = Auth::user();

            View::share('settings', $settings); //Another way to share variables, with the View::share
            View::share('profile', $profile);

            //Now we need to share owr variables trough the REQUEST to our controllers
            $request->merge([
                'settings' => $settings,
                'profile' => $profile,
                'user' => $user
            ]);


        }
        return $next($request);
    }
}

this middleware was inside the middleware groups, so that was the problem hopefully if someone in the future experiments this, then it could check that first.

5
votes

Try below code:

class TrustProxies extends Middleware
{
    protected $proxies = '*';
    protected $headers = Request::HEADER_X_FORWARDED_ALL;
}
1
votes

I just had this problem and turns out that empty parameters in the URL will never validate. So when you do this:

URL::temporarySignedRoute('newsletter.verify', now()->addDays(3), ['name' => $name, 'email' => $email])

but name is an empty string (because it's not mandatory), URL will get generated with name= as part of query string, but this code inside Laravel

$original = rtrim($url.'?'.Arr::query(Arr::except($request->query(), 'signature')), '?');

will not return the empty name, hence the URL was 'altered' and validation fails. The commonly used middleware ConvertEmptyStringsToNull might have something to do with this.

1
votes

I had the same issue and was going insane until I stumbled upon @LaravDev's answer.

Note::I'm using Laravel 7 which is different on the web.php page

My original code looked like this, which essentially just adds a variable to the request to tell my views not to show the sidebar.

Route::middleware(['noSidebar'])->group(function()
{
    Auth::routes(['verify' => true]);
});

I had to remove the Auth::routes() shortcode and switch it out for the full stack of Auth routes. (Note this is different for each version of Laravel)

Route::middleware(['noSidebar'])->group(function()
{

// Authentication Routes...
Route::get('login', 'Auth\LoginController@showLoginForm')->name('login');
Route::post('login', 'Auth\LoginController@login');
Route::post('logout', 'Auth\LoginController@logout')->name('logout');

// Registration Routes...
Route::get('register', 'Auth\RegisterController@showRegistrationForm')->name('register');
Route::post('register', 'Auth\RegisterController@register');

// Password Reset Routes...
Route::get('password/reset', 'Auth\ForgotPasswordController@showLinkRequestForm')->name('password.request');
Route::post('password/email', 'Auth\ForgotPasswordController@sendResetLinkEmail')->name('password.email');
Route::post('password/reset', 'Auth\ResetPasswordController@reset')->name('password.update');

// Confirm Password (added in v6.2)
Route::get('password/confirm', 'Auth\ConfirmPasswordController@showConfirmForm')->name('password.confirm');
Route::post('password/confirm', 'Auth\ConfirmPasswordController@confirm');

// Email Verification Routes...
Route::get('email/verify', 'Auth\VerificationController@show')->name('verification.notice');
Route::post('email/resend', 'Auth\VerificationController@resend')->name('verification.resend');
    
});



//Moved the routes with tokens in the URL to outside my middleware grouping.

Route::get('email/verify/{id}/{hash}', 'Auth\VerificationController@verify')->name('verification.verify');
Route::get('password/reset/{token}', 'Auth\ResetPasswordController@showResetForm')->name('password.reset');

Tada it works! Thank you all

1
votes

I had APP_URL=http://localhost in the .env file. When I changed the value to the URL from the server, the problem was solved.

I was using Laravel 8+.

0
votes

If you're using Heroku, AWS or any other service that makes use of a LoadBalancer. Do also ensure that the proxy reaching your application is trusted.

See this answer for more.

0
votes

Essentially your signatures didn't match because the URL you generated via \Illuminate\Support\Facades\URL::signedRoute was altered by your middleware, meaning when it came to check $request->hasValidSignature() this returned false.

I had a similar issue where SendGrid was adding UTM tracking query strings to the URL's in my email (&utm_campaign=website&utm_source=sendgrid.com&utm_medium=email), which altered the URL and ultimately changes the signature.

As I hack I added the following code to my controller to strip out the additional query params and re-using the signature:

// Fix issue with sendgrid highjacking signed URL's with extra query params..
if ($request->query('utm_campaign')) {
    $sig = $request->query('signature', '');
    $url = route('route-key') . '?signature=' . $sig;

    return redirect($url);
}
0
votes

i had similar problem in dusk, and it was the APP_KEY in .env.dusk.testing that was not matching the APP_KEY in .env

0
votes

UNDERSTANDING LARAVEL EMAIL VERIFICATION WAY

Understanding the way of verification can help you simply solve this error.

laravel makes a temporary signed url using method URL::temporarySignedRoute(),

this method is called in verificationUrl() located at \vendor\laravel\framework\src\Illuminate\Auth\Notifications\VerifyEmail.php.

/**
 * Get the verification URL for the given notifiable.
 *
 * @param mixed $notifiable
 * @return string
 */
protected function verificationUrl($notifiable)
{
   return URL::temporarySignedRoute(
        'verification.verify',
        Carbon::now()->addMinutes(Config::get('auth.verification.expire', 60)),
        [
            'id' => $notifiable->getKey(),
            'hash' => sha1($notifiable->getEmailForVerification()),
        ]
    );
}

URL::temporarySignedRoute() makes urls according to config('app.url) which is set to .env('APP_URL') by default.

So if the url that sent to emails is different from url that will laravel get at the time of verification (time of checking signature of url), 403 | invalid signature occurs.

Example:

  • if you set APP_URL to http://yourdomain.com/, verification link should be look like http://yourdomain.com/email/verify/{id}/{hash}. now if you set your server configs to redirect to https, invalid signature will occured, since the url laravel gets is https://yourdomain.com/email/verify/{id}/{hash} and not same as email verification url.