Scenario:
Same browser.
Tab 1: logging into my laravel application.
Tab 2: logging into my laravel application.
Tab 2: log off
Tab 1: Click a button that causes a redirect to a route that's protected by: Route::group(['middleware' => 'auth'], function () {
...
Result: Laravel 5 crashes before it gets to my code on:
Stack:
Symfony\Component\Debug\Exception\FatalErrorException Call to a member function setCookie() on null
vendor/laravel/framework/src/Illuminate/Session/Middleware/StartSession.php:184 __construct
vendor/laravel/framework/src/Illuminate/Foundation/Bootstrap/HandleExceptions.php:131 fatalExceptionFromError
vendor/laravel/framework/src/Illuminate/Foundation/Bootstrap/HandleExceptions.php:116 handleShutdown
vendor/laravel/framework/src/Illuminate/Foundation/Bootstrap/HandleExceptions.php:0 addCookieToResponse
vendor/laravel/framework/src/Illuminate/Session/Middleware/StartSession.php:72 handle
vendor/laravel/framework/src/Illuminate/Pipeline/Pipeline.php:125 Illuminate\Pipeline\{closure}
vendor/laravel/framework/src/Illuminate/Cookie/Middleware/AddQueuedCookiesToResponse.php:36 handle
vendor/laravel/framework/src/Illuminate/Pipeline/Pipeline.php:125 Illuminate\Pipeline\{closure}
vendor/laravel/framework/src/Illuminate/Cookie/Middleware/EncryptCookies.php:40 handle
vendor/laravel/framework/src/Illuminate/Pipeline/Pipeline.php:125 Illuminate\Pipeline\{closure}
vendor/laravel/framework/src/Illuminate/Foundation/Http/Middleware/CheckForMaintenanceMode.php:42 handle
vendor/laravel/framework/src/Illuminate/Pipeline/Pipeline.php:125 Illuminate\Pipeline\{closure}
vendor/laravel/framework/src/Illuminate/Pipeline/Pipeline.php:101 call_user_func:{/home/vagrant/dev/opus-web-app/vendor/laravel/framework/src/Illuminate/Pipeline/Pipeline.php:101}
vendor/laravel/framework/src/Illuminate/Pipeline/Pipeline.php:101 then
vendor/laravel/framework/src/Illuminate/Foundation/Http/Kernel.php:115 sendRequestThroughRouter
vendor/laravel/framework/src/Illuminate/Foundation/Http/Kernel.php:84 handle
public/index.php:53 {main}
public/index.php:0 [main]
Even when I remove the Route::group(['middleware' => 'auth'] group from my routes... going in tab 1 to the now open URLs will produce this error. I just don't get it.
How do I get rid of this?
I figured out the cause, but I'm just not sure about it. Hoping one of you will know.
In kernel.php:
I had the 'App\Http\Middleware\VerifyCsrfToken' defined in the $middleware black and not the $routeMiddleware. when I moved it to the $routeMiddleware I stopped getting that error.
Content of VerifyCsrfToken:
class VerifyCsrfToken extends BaseVerifier {
/**
* Handle an incoming request.
*
* #param \Illuminate\Http\Request $request
* #param \Closure $next
* #return mixed
*/
public function handle($request, Closure $next)
{
if ($request && (($this->isReading($request) || $this->excludedRoutes($request) || ($this->tokensMatch($request) && Auth::check()))))
{
return $this->addCookieToResponse($request, $next($request));
}
return view('sessionOver');
}
protected function excludedRoutes($request)
{
$routes = [
'deployPush' // webhook push for bitBucket.
];
foreach($routes as $route)
if ($request->is($route))
return true;
return false;
}
}
Seems to me that the problem is your logic in the check of the CSRF.
Here is the problem:
You are checking if the tokens match, and that function is as follow:
protected function tokensMatch($request)
{
$token = $request->input('_token') ?: $request->header('X-CSRF-TOKEN');
if (! $token && $header = $request->header('X-XSRF-TOKEN')) {
$token = $this->encrypter->decrypt($header);
}
return Str::equals($request->session()->token(), $token);
}
But if you look closely you will see that in the return it checks for:
$request->session()->token()
But, the session is null so you will get an exception for trying to request a method from a null.
So, if I'm not mistaken, all you need to do is just to add an additional check to the if in the handle method.
Instead of this:
if ($request && ((
$this->isReading($request)
|| $this->excludedRoutes($request)
|| ($this->tokensMatch($request) && Auth::check())
)))
You should have this:
if ($request && ((
$this->isReading($request)
|| $this->excludedRoutes($request)
|| ($request->hasSession() && $this->tokensMatch($request) && Auth::check())
)))
This way, if there is no session it will not even check if tokens match and it will not crash there.
You may also consider refactor that part of the if statement to a function that by name it describes what you are checking for, and even the complete if statement to a function that says it all. Just for code clarity.
Is either that, or you may consider checking your other Middlewares, anyone that be touching the headers of the request and potentially be unsetting the headers.
If you take at the code of the handle function of the StartSession Middleware:
public function handle($request, Closure $next)
{
$this->sessionHandled = true;
if ($this->sessionConfigured())
{
$session = $this->startSession($request);
$request->setSession($session);
}
// ** HERE ALL OTHER MIDDLEWARES ARE BEING CALL
$response = $next($request);
if ($this->sessionConfigured())
{
$this->storeCurrentUrl($request, $session);
$this->collectGarbage($session);
// ** AFTER ALL OTHER MIDDLEWARES ARE CALLED THE FOLLOWING FUNCTION
// TOUCHES THE HEADER AND IS NULL
$this->addCookieToResponse($response, $session);
}
return $response;
}
So, it is possible that somewhere in any of any other middleware you are touching the header and leaving it null, as per your error says.
I hope that helps.
May be this will usefull for you .check in middleware some think similar like this
public function handle($request, Closure $next)
{
$response = $next($request);
if ( ! Auth::user()) // Your logic here...
abort(403, 'Unauthorized action.');
return $response;
}
Ref::https://laracasts.com/discuss/channels/general-discussion/disable-global-verifycsrftoken-and-use-as-middleware-doesnt-work-setcookie-on-null
in that discussion some cookie error discussed.that might help you
Experienced a similar issue specifically when returning a response from a middleware, and I realised it was caused by returning return view('my.view.blade') rather than return response()->view('my.view.blade').
So in short, don't return the view directly. Chain the view() helper to the response() helper.
return view('myview.blade'); //wrong
return response()->view('myview.blade'); //works
Related
I'm working on a Slim 3 based application with a Twig frontend and I'm also making a REST API.
I've implemented slimphp\Slim-Csrf for the entire app but I now want to exclude this CSRF check from every "API" routes.
I'm trying to implement the "Option 2" of this post :
Slim3 exclude route from CSRF Middleware
Here is the code :
File App\Middleware\CsrfMiddleware.php :
namespace App\Middleware;
class CsrfMiddleware extends \Slim\Csrf\Guard {
public function processRequest($request, $response, $next) {
// Check if this route is in the "Whitelist"
$route = $request->getAttribute('route');
if ($route->getName() == 'token') {
var_dump('! problem HERE, this middleware is executed after the CsrfMiddleware !');
// supposed to SKIP \Slim\Csrf\Guard
return $next($request, $response);
} else {
// supposed to execute \Slim\Csrf\Guard
return $this($request, $response, $next);
}
}
}
File app\app.php :
$app = new \Slim\App([
'settings' => [
'determineRouteBeforeAppMiddleware' => true
]
]);
require('container.php');
require('routes.php');
$app->add($container->csrf);
$app->add('csrf:processRequest');
File app\container.php :
$container['csrf'] = function ($container) {
return new App\Middleware\CsrfMiddleware;
};
File app\routes.php :
<?php
$app->get('/', \App\PagesControllers\LieuController::class.':home')->setName('home');
$app->post('/api/token', \App\ApiControllers\AuthController::class.'postToken')->setName('token');
When I do a POST request on http://localhost/slim3/public/api/token I've got :
Failed CSRF check!string(70) "! problem HERE, this middleware is executed after the CsrfMiddleware !"
Like if my CsrfMiddleware was executed after \Slim\Csrf\Guard...
Anyone has an idea ?
Thank you.
In Slim 3 the middleware is LIFO (last in first out).
Add the middleware in the opposite direction:
Before
$app->add($container->csrf);
$app->add('csrf:processRequest');
After
$app->add('csrf:processRequest');
$app->add($container->csrf);
Notice: The public directory should not be part of the url
Not correct: http://localhost/slim3/public/api/token
Correct: http://localhost/slim3/api/token
To skip the processing within the middleware, just return the $response object.
// supposed to SKIP \Slim\Csrf\Guard
return $response;
Here is how I achieved this with Slim 3.
1) Create a class that extends \Slim\Csrf\Guard as follows.
The CsrfGuardOverride class is key to enabling or disabling CSRF checking for a path. If the current path is whitelist'ed, then the __invoke() method just skips the core CSRF checking, and proceeds by executing the next middleware layer.
If the current path is not in the whitelist (i.e., CSRF should be checked), then the __invoke method defers to its parent \Slim\Csrf\Guard::__invoke() to handle CSRF in the normal manner.
<?php
namespace App\Middleware;
use Psr\Http\Message\ResponseInterface;
use Psr\Http\Message\ServerRequestInterface;
use \Slim\Csrf\Guard;
class CsrfGuardOverride extends Guard {
/**
* Invoke middleware
*
* #param ServerRequestInterface $request PSR7 request object
* #param ResponseInterface $response PSR7 response object
* #param callable $next Next middleware callable
*
* #return ResponseInterface PSR7 response object
*/
public function __invoke(ServerRequestInterface $request, ResponseInterface $response, callable $next)
{
// Set the name of the route we want whitelisted with a name
// prefix of 'whitelist'. check for that here, and add
// any path to the white list
$route = $request->getAttribute('route');
$routeName = $route->getName();
$whitelisted = strpos($routeName, 'whitelist');
// if url is whitelisted from being CSRF checked, then bypass checking by skipping directly to next middleware
if ($whitelisted !== FALSE) {
return $next($request, $response);
}
return parent::__invoke($request, $response, $next);
}
}
2) Register the CsrfGuardOverride class. Be sure to set settings.determineRouteBeforeAppMiddleware => true as this forces Slim to evaluate routes prior to executing any middleware.
// Method on App Class
protected function configureContainer(ContainerBuilder $builder)
{
parent::configureContainer($builder);
$definitions = [
'settings.displayErrorDetails' => true,
'settings.determineRouteBeforeAppMiddleware' => true,
// Cross-Site Request Forgery protection
\App\Middleware\CsrfGuardOverride::class => function (ContainerInterface $container) {
$guard = new \App\Middleware\CsrfGuardOverride;
$guard->setPersistentTokenMode(true); // allow same CSRF token for multiple ajax calls per session
return $guard;
},
'csrf' => DI\get(\App\Middleware\CsrfGuardOverride::class),
// add others here...
];
$builder->addDefinitions($definitions);
}
3) Add the path that you want to by bypass CSRF checking and give it a name with the prefix 'whitelist':
$app->post('/events/purchase', ['\App\Controllers\PurchaseController', 'purchaseCallback'])->setName('whitelist.events.purchase');
i use Laravel passport for auth
in route api.php
Route::get('/todos', function(){
return 'hello';
})->middleware('auth:api');
but when open localhost:8000/api/todos I see the following error
InvalidArgumentException
Route [login] not defined.
* #return string
*
* #throws \InvalidArgumentException
*/
public function route($name, $parameters = [], $absolute = true)
{
if (! is_null($route = $this->routes->getByName($name))) {
return $this->toRoute($route, $parameters, $absolute);
}
throw new InvalidArgumentException("Route [{$name}] not defined.");
}
/**
* Get the URL for a given route instance.
*
* #param \Illuminate\Routing\Route $route
* #param mixed $parameters
* #param bool $absolute
* #return string
*
* #throws \Illuminate\Routing\Exceptions\UrlGenerationException
*/
protected function toRoute($route, $parameters, $absolute)
{
return $this->routeUrl()->to(
$route, $this->formatParameters($p
I want if the user was not authenticated
Do not redirect to any page and only see the page
Use Postman and set the Header Accept: application/json otherwise Laravel Passport would never know it's an API client and thus redirect to a /login page for the web.
see below image to see where to set the accept parameter:
Did you enter above-mentioned URL directly in browser search bar?
If you did its wrong way because you also need to enter API token with your request__!!
To check either request includes token or not make your own middleware.
Command to create Middleware
php artisan make:middleware CheckApiToken
https://laravel.com/docs/5.6/middleware
change middleware handle method to
public function handle($request, Closure $next)
{
if(!empty(trim($request->input('api_token')))){
$is_exists = User::where('id' , Auth::guard('api')->id())->exists();
if($is_exists){
return $next($request);
}
}
return response()->json('Invalid Token', 401);
}
Like This
Your Url should be like this
http://localhost:8000/api/todos?api_token=API_TOKEN_HERE
You also have to add another header Key: Accept and value: application/json
Check Your Header Request to put
Authorization = Bearer {your token}
In the following of #Eki answer,
This error is because you didn't set "Accept" field in your headers.
To avoid this error, add a middleware with priority to Authenticate to check that:
add an extra middleware with below handler
public function handle($request, Closure $next)
{
if(!in_array($request->headers->get('accept'), ['application/json', 'Application/Json']))
return response()->json(['message' => 'Unauthenticated.'], 401);
return $next($request);
}
set priority in app/Http/Kernel.php
protected $middlewarePriority = [
...
\App\Http\Middleware\MyMiddleware::class, // new middleware
\App\Http\Middleware\Authenticate::class,
...
];
add new middleware to your route
Route::get('/todos', function(){
return 'hello';
})->middleware('MyMiddleware', 'auth:api');
You also have to add another header Key: X-Requested-With and value: XMLHttpRequest
Is there a way to provide in Laravel 5.5 what 4.2 had with:
Route::when('*-ajax', 'ajax');
I had an option to attach a filter to all ajax routes, and since then I know that filters have been replaced with middleware, but I am getting an error that:
Attribute [when] does not exist.
You can use Request::is(). Few details in the doc
It accept multiple params as well.
[edit] You also have Request::ajax() which returns true when you perform an ajax request
I found a solution in having implemented a global middleware for this:
public function handle($request, Closure $next)
{
$parsedUrl = parse_url($request->url());
if (key_exists('path', $parsedUrl) && strpos($parsedUrl['path'], '-ajax') !== false && !$request->ajax()) {
return response('Bad request', 400);
}
return $next($request);
}
I'm using Laravel 5.3 & Passport.
When using Postman to test any route I have set in api.php file it always returns the login page. Here is an example of my testing route:
Route::get('/getKey', function() {
return 'hello';
})->middleware('client_credentials');
Postman params:
Accept application/json
Authorization Bearer <then my key>
I have set middleware to 'auth:api' per another solution I found while searching for the answer.
protected function mapApiRoutes()
{
Route::prefix('api')
->middleware('auth:api')
->namespace($this->namespace)
->group(base_path('routes/api.php'));
}
I've tried just about every solution that has worked for others but still no luck. Any suggestions would be much appreciated.
UPDATE
So I finally got something to work. I created a consumer app and created a few test functions. I was able to consume the api, with verification of token. However, hitting this Route no longer returns my login page, but instead now returns nothing. So its still not working for whatever reason.
Route::get('/user', function (Request $request) {
return $request->user();
})->middleware('client_credentials');
The redirection to the defined login route is occurring in the app\Exceptions\Handler.php class.
protected function unauthenticated($request, AuthenticationException $exception)
{
if ($request->expectsJson()) {
return response()->json(['error' => 'Unauthenticated.'], 401);
}
return redirect()->guest(route('login'));
}
The function tries to detect whether it is being called from an API (it which case it returns a 401 Unauthorized reponse with JSON message) and if not it will redirect to the login page according to the comments it
Converts an authentication exception into an unauthenticated response
To resolve the issue in postman, on the request click on the Headers tab and add:
key: Accept
value: application/json
I'm pretty new to this so am not sure whether this is a header we should be adding when testing all API calls with Postman or just a nusience with how this laravel method is setup.
Anyway this would solve your issue with being redirected to the login page, however it's a sign your underlying authentication isn't working
You need to add Authorization: Bearer YOUR_TOKEN to your every request's header. Also, for latest version Laravel 5.5 or above. You need to add Accept: application/json to request header too.
Add this code on Headers on postman.
key Value
Accept application/json
It is coded to check whether the request comes from Ajax. In that case you will receive the following json if authentication fails:
{
"error": "Unauthenticated."
}
Otherwise it will assume you are using a browser and it will redirect to Html login page for authentication.
You can add the following header to your request to simulate an Ajax request:
X-Requested-With = XMLHttpRequest
From laravel 5.8 till the current 6.0 version there is a a middleware located at the app/http/Middleware which is \App\Http\Middleware\Authenticate, it has a method
redirectTo
with the code
protected function redirectTo($request)
{
if (! $request->expectsJson()) {
return route('login');
}
}
Re-write this to
protected function redirectTo($request)
{
if (! ($request->expectsJson() || collect($request->route()->middleware())->contains('api'))) {
return route('login');
}
}
What this code does is to return a Illuminate\Routing\Redirector instance and sets it as the \Illuminate\Auth\AuthenticationException $redirectTo parameter . which is passed to \App\Exceptions#render by laravel.
At the render function you can write a logic to catch an \Illuminate\Auth\AuthenticationException exception.
Here is my own implementation
/**
* Render an exception into an HTTP response.
*
* #param \Illuminate\Http\Request $request
* #param \Exception $exception
* #return \Illuminate\Http\Response
*/
public function render($request, Exception $exception)
{
/**
* Render an Authentification exception when user trying to viditing a route or
* Perform an action is not properly authenticated
*/
if ($exception instanceof \Illuminate\Auth\AuthenticationException) {
return $this->unauthenticated($request,$exception);
}
}
/**
* Convert an authentication exception into a response.
*
* #param \Illuminate\Http\Request $request
* #param \Illuminate\Auth\AuthenticationException $exception
* #return \Symfony\Component\HttpFoundation\Response
*/
protected function unauthenticated($request, \Illuminate\Auth\AuthenticationException $exception)
{
return $exception->redirectTo()
? redirect()->guest($exception->redirectTo())
: response()->json(['message' => $exception->getMessage()], 401);
}
As noted in many of the answers the reason this happens is indeed the code:
if (! $request->expectsJson()) {
return route('login');
}
in app\Http\Middleware\Authenticate.php
One way to solve this is to wrap your api requests in Middleware that adds 'Accept: application/json' to the header of those requests.
I got this idea from this article: https://medium.com/#martin.riedweg/laravel-5-7-api-authentification-with-laravel-passport-92b909e12528
Laravel version: 9.*
// app/Http/Middleware/Authenticate.php
protected function redirectTo($request)
{
return '/api/unauthenticated'; // You can name whatever route you want
}
// routes/api.php
Route::get('/unauthenticated', function () {
return response()->json(['message' => 'Unauthenticated.'], 403);
});
Hope this helps.
Happy coding!
If you don't want to enforce your api consumer to pass Accept - application/json in all the request headers you can add the following method in App\Exceptions\Handlers.php; to customize error response
//use Illuminate\Auth\AuthenticationException;
protected function unauthenticated($request, AuthenticationException $exception)
{
if ($request->is('api/*')) {
return response()->json(['error' => 'Invalid token', 'status' => false], 401);
}
return redirect()->guest(route('login'));
}
if you are using username instead of email for credential; insert this method at your User model:
function findForPassport($username) {
return $this->where('username', $username)->first();
}
I am trying to cache entire response using middleware
Steps i followed
Generated two middleware
AfterCacheMiddleware
BeforeCacheMiddleware
With in BeforeCacheMiddleware:
public function handle($request, Closure $next)
{
$key = $request->url();
if(Cache::has($key)) return Cache::get($key);
return $next($request);
}
With in AfterCacheMiddleware
public function handle ($request, Closure $next)
{
$response = $next($request);
$key = $request->url();
if (!Cache::has($key)) Cache::put($key, $response->getContent(), 60);
return $response;
}
Registered middleware in $routeMiddleware array of kernal.php
'cacheafter' => 'App\Http\Middleware\AfterCacheMiddleware',
'cachebefore' => 'App\Http\Middleware\BeforeCacheMiddleware',
With in routes.php i am calling this dummy routes like this
Route::get('middle', ['middleware' => 'cachebefore', 'cacheafter', function()
{
echo "From route";
}]);
Issue:
only cachebefore middleware is getting called. cacheafter is not getting called at all
Can anyone suggest what i am missing here ?
I found this question while I was looking for solution myself. I am aware that there is the Flatten package which does this caching, but I couldn't find nice examples on how to do this by myself. The solution attempt in this question contains ideas that were useful for my own solution, although I chose to go with a single middleware only.
Although the question is old and asker probably doesn't need the answer anymore, I will share my solution here as I feel that SO (and internet) lacks this caching sample for Laravel 5. I will try to explain as much as I can, but for most benefit you should be familiar with Routing, Caching and Middlewaring in Laravel 5. So here goes the solution:
Middleware
Create a middleware, those are usually placed in app/Http/Middleware folder and I will call the file CachePage.php
<?php
namespace App\Http\Middleware;
use Closure;
use Illuminate\Contracts\Auth\Guard;
use Cache;
class CachePage
{
public function handle($request, Closure $next)
{
$key = $request->fullUrl(); //key for caching/retrieving the response value
if (Cache::has($key)) //If it's cached...
return response(Cache::get($key)); //... return the value from cache and we're done.
$response = $next($request); //If it wasn't cached, execute the request and grab the response
$cachingTime = 60; //Let's cache it for 60 minutes
Cache::put($key, $response->getContent(), $cachingTime); //Cache response
return $response;
}
}
Change $key according to your needs... You have all the $request with all the parameters... Change Cache::put($key, $value, $minutes) to Cache::forever($key, $value) if you will clear the cache manually and don't want it to ever expire.
Using the URL as key for storing cache is usable in most cases, but one might think of something more appropriate for a certain project.
Registering middleware
Register it in app/Http/Kernel.php by adding the middleware to $routeMiddleware array like this:
protected $routeMiddleware = [
/* ... */
/* Other middleware that you already have there */
/* ... */
'cachepage' => \App\Http\Middleware\CachePage::class,
];
Of course, you should change \App\Http\Middleware\CachePage if you placed it elsewhere or gave it another name. Also the key name cachepage is up to you - it will be used to invoke the middleware.
Usage
In your app/Http/routes.php use the middleware just like auth or other middlewares, for example, you might make a route group for all the pages that should be cached:
Route::group(['middleware' => 'cachepage'], function ()
{
Route::get('/', 'HomeController#home');
Route::get('/contacts', 'SectionController#contacts');
});
The list of middlewares has to be inside square brackets:
Route::get('middle', ['middleware' => ['cachebefore', 'cacheafter'], function()
{
echo "From route";
}]);