Laravel 403 Invalid Signature only on Nginx Web Server - php

Problem
I am creating a signed URL through laravels 'temporarySignedRoute' function copied and pasted from laravels signed routes documentation, on my web server the url comes out to a 403 invalid signature. The signed URL's work on my local machine but upon uploading it to my test server it gives the error shown below.
What I've tried
I have been searching for a solution for awhile now and have tried many that have been posted on stack overflow but nothing seems to solve my problem.
I have tried all of the following:
Forcing all routes to https through the AppServiceProvider, this forced all of the url's to be https but gave the same result.
I tried changing Nginx's configuration as described here.
I also tried adding the TrustProxies middleware with the proxies configured to '*' or all, like described in laravel's documentation on Trust Proxies, same result.
Stopped the web server from forcing the domain to https to test if it was just https causing it, same result.
-- UPDATE --
I went into vendor/laravel/framework/src/Illuminate/Routing/UrlGenerator.php while trying to search around for any clues in the project. I decided to datadump (dd) out the tokens it was comparing on my local and hosted projects and this was the result. Code shown below as well.
public function hasValidSignature(Request $request, $absolute = true)
{
$url = $absolute ? $request->url() : '/'.$request->path();
$original = rtrim($url.'?'.Arr::query(
Arr::except($request->query(), 'signature')
), '?');
$expires = $request->query('expires');
$signature = hash_hmac('sha256', $original, call_user_func($this->keyResolver));
dd($signature . " :: " . $request->query('signature', ''));
return hash_equals($signature, (string) $request->query('signature', '')) &&
! ($expires && Carbon::now()->getTimestamp() > $expires);
}
Local Project
Hosted Project
So the Token is not being passed through on my web server but is on my local. Also another thing I noticed is that the web servers token is always the same and never changes no matter what.
My current Nginx configuration can be viewed here.
My current virtual hosts configuration is here.

I think this might be the issue.
Try to change your location block in the config file to
try_files $uri $uri/ /index.php?$query_string;
I think you have missed the ? between index and query string.
Update this and make sure to reload your Nginx server.
sudo service nginx reload
Let me know if this works.

The post from Prince Kumar Barnwal fixed my issue back then, I recently rediscovered another version of this issue while attempting to use docker in the same setup and I thought I would post it here in case anyone has the same issue.
We are using a Proxy Server and certifying our docker laravel projects on that, all projects have the TrustProxies setup, then we forceHTTPS in the AppServiceProvider.php which makes AJAX calls work. This causes issues as the project itself is not encrypted but all urls it generates thinks it is since we are forcing it. When the proxy passes the URL in the request it is http, when the project expects HTTPS.
I found a solution to this, its not the most clean and theirs probably a better fix out there but I made a middleware that goes on all of the projects signed routes, basically it just changes the url in the request to be secure if the project is in production. This is not insecure as the proxy handles the security and this is ONLY on the signed routes.
Below is the middleware that goes ONLY on the signed routes.
<?php
namespace App\Http\Middleware;
use Closure;
class SecureSignedURL
{
/**
* Handle an incoming request.
*
* #param \Illuminate\Http\Request $request
* #param \Closure $next
* #return mixed
*/
public function handle($request, Closure $next)
{
if(config('app.env') === 'production' || config('app.env') === 'staging') {
$request->server->set('HTTPS', 'ON');
}
return $next($request);
}
}
My web.php with the middleware, its defined as secure_signed_url in my Kernel.php
Route::group(['middleware' => ['secure_signed_Url', 'signed']], function () {
route::get('/exampleurl1', 'TestController#route_exampleurl1_signed')->name('exampleurl1.signed');
route::get('/exampleurl2', 'Template\TemplateController#route_exampleurl2_signed')->name('exampleurl2.signed');
});
Again I don't know if this is the best fix, this is a very obscure bug in Laravel. If you have any suggestions for a better way of fixing this I'm open to suggestions.
My Old Github Issue that may help: https://github.com/laravel/framework/pull/31434

This is not a Nginx error. That's how laravel handles invalid signed urls if you check the Illuminate\Routing\Middleware\ValidateSignature it throws an Illuminate\Routing\Exceptions\InvalidSignatureException which aborts the request with a 403 status. What I did,
Created a custom signed middleware which extends the actual ValidateSignature
<?php
namespace App\Http\Middleware;
use App\Exceptions\InvalidEmailVerificationException;
use Closure;
use Illuminate\Routing\Middleware\ValidateSignature as MiddleWare;
class ValidateSignature extends Middleware
{
/**
* Handle an incoming request.
*
* #param \Illuminate\Http\Request $request
* #param \Closure $next
* #return mixed
*/
public function handle($request, Closure $next)
{
if ($request->hasValidSignature()) {
return $next($request);
}
throw new InvalidEmailVerificationException;
}
}
which throws a custom App\Exceptions\InvalidEmailVerificationException
<?php
namespace App\Exceptions;
use Symfony\Component\HttpKernel\Exception\HttpException;;
class InvalidEmailVerificationException extends HttpException
{
/**
* Create a new exception instance.
*
* #return void
*/
public function __construct()
{
parent::__construct(403, 'Your email verification token seems to be invalid.');
}
}
then change the signed middleware in your App\Http\Kernel.php $routeMiddleware to 'signed' => \App\Http\Middleware\ValidateSignature::class,
in your 403.blade.php view, you can display the message as
#if ($exception instanceof \App\Exceptions\InvalidEmailVerificationException)
<h1 class="text-ginormous">Snap!</h1>
<h4 class="quick-error-description">Verification token expired.</h4>
#if (session('resent'))
<div class="alert alert-success" role="alert">
{{ __('A fresh verification link has been sent to your email address.') }}
</div>
#endif
<p class="error-description">{{ $exception->getMessage() }}</p>
<a href="{{ route('verification.resend') }}" class="button-solid button-error mb-3" data-no-pjax>Email me a new link</a>
#endif

Related

Laravel redirect away method redirects to own domain

I am coding a Laravel API hosted on api.url.com for an application hosted on www.myurl.com on a server different to the Lravel API using Laravel Fortify.
The problem comes when the user verifies their email on the generated link, they are redirected not to the external application but again back to the API, but on their browser.
THe docs state that this configuration in the Authenticate.pgp Middleware would work:
<?php
namespace App\Http\Middleware;
use Illuminate\Auth\Middleware\Authenticate as Middleware;
class Authenticate extends Middleware
{
/**
* Get the path the user should be redirected to when they are not authenticated.
*
* #param \Illuminate\Http\Request $request
* #return string|null
*/
protected function redirectTo($request)
{
if (! $request->expectsJson()) {
return url(env('SPA_URL') . '/dashboard');
}
}
}
Where SPA_URL is set in the .env to www.myurl.com.
This redirects to api.url.com/www.myurl.com/dashboard Obviously causing a 404 response.
I then tried to call an action on a controller as such
public function redirectDashboard(Request $request){
return redirect()->away('www.myurl.com/dashboard');
}
This again redirects to api.url.com/www.myurl.com/dashboard Obviously causing a 404 response.
I have no clue why redirect()->away would still redirect to a url on the same API domain.
Any help would be appreciated, thank you.
The RedirectTo method will redirect to request.
Try to write new middleware
public function handle($request, Closure $next)
{
//check here if the user is authenticated
if ( ! $this->auth->user() )
{
// here you should redirect to another site
return redirect(env('SPA_URL') . '/dashboard');
}
return $next($request);
}
I finally realised what was happening with the redirect: I was using the redirect() helper function rather than the Redirect::class. The helper function appends the current domain to any url or away method call whereas the Redirect::class allows you to redirect to anywhere you need.

/broadcasting/auth endpoint always fails with 403 (access denied) error when trying to authenticate with Sanctum

I haven't been able to find too many answers on specifically authenticating broadcasting using Sanctum. I'm trying to implement a basic event that broadcasts to pusher, but I keep getting a 403 error when trying to connect to /broadcasting/auth. Firstly, yes, I have uncommented the BroadcastServiceProvider in /config/app.php. Here is an extract from my Broadcast service provider:
app/Providers/BroadcastServiceProvider.php
class BroadcastServiceProvider extends ServiceProvider
{
/**
* Bootstrap any application services.
*
* #return void
*/
public function boot()
{
Broadcast::routes(['middleware' => ['auth:sanctum']]);
require base_path('routes/channels.php');
}
}
I authenticate using Sanctum and all of my endpoints live in the api.php file. I'm not sure whether I should be declaring the Broadcast::routes there, as I've seen some people allude to it, but I don't really know what difference it makes as it still gives me the same error. I also don't even know if calling auth:sanctum middleware is valid here. I mean, I know it works for all of my api routes, but I'm not sure about in this Provider file.
I'm trying to hit the /broadcasting/auth endpoint with Postman, while including my Sanctum Authorization header (Bearer [token]) in the request, but it just gives me a 403.
Can someone please point me in the right direction here?
I also had a similar issue but I was able to resolve it by adding the channel on which I'm broadcasting on in the channel.php file inside the route directory.
channel.php
Broadcast::channel('request_channel.{device_id}', function ($user, $device_id) {
return (int) $user->id === (int) \App\UserDevice::find($device_id)->user_id;
});

Why is my webhook working on my local but not on production?

I am trying to set up a webhook for my project (Laravel 5.8, nginx on Production and XAMPP for local), whereby a service will send a POST payload to my desired url (/webhooks/revolut) when there is an update with a payment status.
I'm starting with the basics and have allowed a POST request to my URL (/webhooks/revolut) which would just return a JSON object with a success message.
I've created this in Laravel, by creating the POST route for the webhook, disabling CSRF verification for this one route and creating the webhook controller to return the JSON string.
When testing using Postman it works when using my local server, but once pushed live to my project, I am getting a "405 - Method Not Allowed" error (The GET method is not supported for this route. Supported methods: POST). But I am sending a POST request to the server.
The Controller does not have any additional middleware on it and does not require the user to be logged in to access the page. I am thinking this could be an authentication error though.
I have tried changing the URL (some forums said that trailing slashes can cause an issue) and Checking the middleware on the controller. I'm not sure what else I can try as from my 5 hour long search, there is little information on where to begin.
My route:
Route::post('/webhooks/revolut', 'Controller#webhookNotification')->name('api');
My Controller:
public function webhookNotification(Request $request)
{
return response()->json([
"message" => "API Successful."
], 201);
}
My VerifyCRSF Middleware:
class VerifyCsrfToken extends Middleware
{
/**
* Indicates whether the XSRF-TOKEN cookie should be set on the response.
*
* #var bool
*/
protected $addHttpCookie = true;
/**
* The URIs that should be excluded from CSRF verification.
*
* #var array
*/
protected $except = [
'/webhooks/revolut',
];
}
I'm confused as to why it is working on the local server but not the production. Could this be a nginx setup issue on the live server?

Laravel 5.7 signed route returns 403 invalid signature

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:
Try the route without signing, and works perfectly
Try the route without parameters and only signing
Try the route without temporary setting and only signing
Set cloudflare's ip to trusted proxies
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.
Try below code:
class TrustProxies extends Middleware
{
protected $proxies = '*';
protected $headers = Request::HEADER_X_FORWARDED_ALL;
}
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.
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.
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.
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.
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
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
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+.
protected $proxies = '*';
Change this code on below path
app/Http/Middleware/TrustProxies.php
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);
}

How to serve static content in Laravel 5 over https?

I've spent a ton of time trying to fix this but haven't had any luck so far. My app isn't loading css because of a mixed content error (The page at 'https://example.com/' was loaded over HTTPS, but requested an insecure stylesheet 'http://example.com/assets/css/magazine.min.css'. This request has been blocked; the content must be served over HTTPS). I know that I can load the assets by passing in true to the asset function but that would mean I would have to go to all the asset calls and change them. Is there a site wide setting I can configure so that it does https in production and http in local?
Thanks
You can create something like ForceHttps middleware and than create condition for environment inside of it, like this:
public function handle($request, Closure $next)
{
if (!\App::environment('local')) {
\URL::forceSchema('https');
}
return $next($request);
}
Than add it to some route group or globally if you want.
NOTE: I would suggest to resolve this on your web server, not in Laravel
Just use asset() helper to generate asset's URL. It will use the current protocol.
Do NOT force assets to be loaded by https, unless they are sensitive (which is almost never the case). That would be an overhead, because you usually care more of safe website content than safe assets. In other words, if you accept loading http website, you most likely accept http assets. Instead, consider using middleware to redirect http to https on each non-safe request.
This is the middleware I'm using myself:
public function handle($request, Closure $next)
{
if (!$request->secure()) {
return redirect()->secure($request->getRequestUri());
}
return $next($request);
}
If you wish to use it, please remember to fire it BEFORE attaching any cookies, that is before Illuminate\Cookie\Middleware\AddQueuedCookiesToResponse middleware.
I created app/Helpers/SiteHelpers.php containing a function that overrides the default asset() function.
<?php
/**
* Overrides the default asset() method, which generates an asset path for the application.
*
* #param string $path
* #param bool $secure
*
* #return string
*/
function asset ($path, $secure = null) {
if (Request::server('HTTP_X_FORWARDED_PROTO') == 'https' || Request::server('HTTPS') == 'on') {
$secure = TRUE;
}
return app('url')->asset($path, $secure);
}
then added it on bootstrap/autoload.php above require __DIR__.'/../vendor/autoload.php'; so it would look like below:
require __DIR__.'/../app/Helpers/SiteHelpers.php';
require __DIR__.'/../vendor/autoload.php';
this is flexible depending on whether you are serving your static content on http or https

Categories