I am checking an ACL in the pre-dispatch method of an ACL Action Helper. If the action is-allowed, the controller/action should continue as per normal. (No problems there). If it's NOT allowed, however, I would like to:
leave the requsted URI in the browser
skip executing the requested action method
generate an 'access denied' message
At first I thought I would just call _forward() in the Action Helper, but I can't since it's a protected method. The View Renderer Action Helper says that gotoSimple is like _forward, however it still performs the full http redirect (and thus changes the URI in the browser).
I could try calling setScriptPath() on View, then Render(), however this would not prevent the requested controller/action from firing. I think there's probably a straightforward answer to this, but it's beyond my level of experience!
Any assistance appreciated!
This is most easily solved by simply throwing a Zend_Controller_Action_Exception, preferably with code 401 (Unauthorized).
This will be caught by the error handler plugin and forwarded to the Error controller.
You can then check for this error code and handle it appropriately. This is in my error controller
if ($errors->exception->getCode() == 401) {
$this->getResponse()->setHttpResponseCode(401);
return $this->_forward('unauthorized');
}
The "unauthorized" action just displays a view but you could do more with it (log an error for example)
Related
Framework: Laravel 9.x
I'm attempting to use custom error pages. These are located in the default location: /resources/views/errors/....
These load as expected. However, they extend the base view template. This template uses the auth()->user() functionality to make decisions on visible menus.
The auth()->user() does not work on the error views. Note that it works as expected on other views.
How do I load the authenticated user for these error file views?
Thanks in advance.
Add this at the end of your route file for 404 error. Usually 404 doesn't get the session.
Route::fallback(function () {
return abort(404);
});
All other errors such as 401, 403, 419, 429, 500, 503 these gets the session by default you do not need to do anything for that. Basically 404 means you do not have that route so system kicks user out of the system, in that case kernel does not load with all providers. if provider do not load you will not get session. So for 404 we are telling our system let our user stay but if the page is not available use fallback route.
If you want to test it for each errors you can do something like this.
Create a blank route
Route::get('/blank', fn() => abort(401));
go to 401.blade.php change message to auth()->user()->id.
visit the /blank URL.
If you see your id your session is working.
I created customs Twig templates for http error display to keep the site design unified by extending my base layout. (I want to keep my navigation menu and display the error, unlike the regular error messages)
It's working as expected but for the 404.
In the navigation menu of my base layout, I have a lot of is_granted('SOME_ROLES') to display the availables sections of the site depending of user's rights. When a 404 is thrown, the navigation menu is displayed as if the user is disconnected : {% if is_granted("IS_AUTHENTICATED_REMEMBERED") %} being false.
After some searches, I found that the router is executed before the firewall. Since no route is found when a 404 is thrown, the firewall isn't executed and the rights aren't send to the template.
The only workaround I found (source from 2014) is to add at the very bottom of the routes.yaml file this route definition :
pageNotFound:
path: /{path}
defaults:
_controller: App\Exception\PageNotFound::pageNotFound
Since every other routes hasn't match, this one should be the not found.
The controller :
<?php
namespace App\Exception;
use Symfony\Component\HttpKernel\Exception\NotFoundHttpException;
class PageNotFound
{
public function pageNotFound()
{
return (new NotFoundHttpException());
}
}
Because a controller is executed, the firewall is executed and the 404 error page is shown as I expected (hooray !).
My question is : Is there any proper way to fix that issue instead of that workaround?
We had a similar issue.
We wanted to have access to an authentication token in error pages.
In the scenario where part of the website is behind a firewall, say example.com/supersecretarea/, we wanted than unauthorized users get a 403 error code when accessing any url behind example.com/supersecretarea/, even in the event that the page doesn't exist. Symfony's behavior does not allow that and checks for a 404 (either because there is no route or because the route has parameter which didn't resolve, like example.com/supersecretarea/user/198 when the is no user 198).
What we ended up doing was to override the default router in Symfony (Symfony\Bundle\FrameworkBundle\Routing\Router) to modify its behavior:
public function matchRequest(Request $request): array
{
try {
return parent::matchRequest($request);
} catch (ResourceNotFoundException $e) {
// Ignore this next line for now
// $this->targetPathSavingStatus->disableSaveTargetPath();
return [
'_controller' => 'App\Controller\CatchAllController::catchAll',
'_route' => 'catch_all'
];
}
}
CatchAllController simply renders the 404 error page:
public function catchAll(): Response
{
return new Response(
$this->templating->render('bundles/TwigBundle/Exception/error404.html.twig'),
Response::HTTP_NOT_FOUND
);
}
What happens is that during the regular process of Symfony's router, if something should trigger a 404 error, we catch that exception within the matchRequest function. This function is supposed to return information about which controller action to run to render the page, so that's what we do: we tell the router that we want to render a 404 page (with a 404 code). All the security is handled in between matchRequest returning and catchAll being called, so firewalls get to trigger 403 errors, we have an authentication token, etc.
There is at least one functional issue to this approach (that we managed to fix for now). Symfony has an optional system that remembers the last page you tried to load, so that if you get redirected to the login page and successfully log in, you'll be redirected to that page you were trying to load initially. When the firewall throws an exception, this occurs:
// Symfony\Component\Security\Http\Firewall\ExceptionListener
protected function setTargetPath(Request $request)
{
// session isn't required when using HTTP basic authentication mechanism for example
if ($request->hasSession() && $request->isMethodSafe(false) && !$request->isXmlHttpRequest()) {
$this->saveTargetPath($request->getSession(), $this->providerKey, $request->getUri());
}
}
But now that we allow non-existing pages to trigger firewall redirections to the login page (say, example.com/registered_users_only/* redirects to the loading page, and an unauthenticated user clicks on example.com/registered_users_only/page_that_does_not_exist), we absolutely don't want to save that non-existing page as the new "TargetPath" to redirect to after a successful login, otherwise the user will see a seemingly random 404 error. We decided to extend the exception listener's setTargetPath, and defined a service that toggles whether a target path should be saved by the exception listener or not.
// Our extended ExceptionListener
protected function setTargetPath(Request $request): void
{
if ($this->targetPathSavingStatus->shouldSave()) {
parent::setTargetPath($request);
}
}
That's the purpose of the commented $this->targetPathSavingStatus->disableSaveTargetPath(); line from above: to turn the default-on status of whether to save target path on firewall exceptions to off when there's a 404 (the targetPathSavingStatus variables here point to a very simple service used only to store that piece of information).
This part of the solution is not very satisfactory. I'd like to find something better. It does seem to do the job for now though.
Of course if you have always_use_default_target_path to true, then there is no need for this particular fix.
EDIT:
To make Symfony use my versions of the Router and Exception listener, I added the following code in the process() method of Kernel.php:
public function process(ContainerBuilder $container)
{
// Use our own CatchAll router rather than the default one
$definition = $container->findDefinition('router.default');
$definition->setClass(CatchAllRouter::class);
// register the service that we use to alter the targetPath saving mechanic
$definition->addMethodCall('setTargetPathSavingStatus', [new Reference('App\Routing\TargetPathSavingStatus')]);
// Use our own ExceptionListener so that we can tell it not to use saveTargetPath
// after the CatchAll router intercepts a 404
$definition = $container->findDefinition('security.exception_listener');
$definition->setClass(FirewallExceptionListener::class);
// register the service that we use to alter the targetPath saving mechanic
$definition->addMethodCall('setTargetPathSavingStatus', [new Reference('App\Routing\TargetPathSavingStatus')]);
// ...
}
I have a function that calls our API called executeGetRequest (with variants for other HTTP methods). It is static and located in a base class. It is called from Controllers. If the API returns a 401 HTTP status code, executeGetRequest should redirect the user to the logout page on the "frontend" Yii2 project in order to clear the out of date session data.
My coworker and I have tried multiple different ways to redirect. $this->redirect() will not work because there is no $this object from a static context.
return Yii::$app->getResponse()->redirect(Url::to('login/user-logout'));
does not work.
Yii::$app->getResponse()->redirect(Url::to('login/user-logout'))->send();
return;
does not work. We tried these with and without Url::to().
I was able to get a Yii::trace() in the conditional that checked for the 401 response. It works fine. So the problem is not the detection of 401 status codes, but the redirect.
This should work
Yii::$app->response->redirect(['login/user-logout'])->send(); return;
Possible causes why it's not working in your case:
This was ajax/pjax request.
You have used redundant Url::to() with wrong route.
You are expecting POST request which is often the case with logout actions.
401 means it's probably #3. Try the same redirection mechanism but pointing to other route and/or remove verb behavior for user-logout POST action to verify it.
I'm currently migrating a project from CodeIgniter to Laravel5.
I saw in Laracasts that you can use the Request::authorize() method to authorize access before the controller is called, and it returns true or false.
This would (I think) be the ideal solution as I can contain permission checks within the request, rather than pollute the controller with permission checks and redirections / responses.
The only problem is, when I return false from authorize(), it simply loads an empty white page with forbidden written, and I can't find any documentation on laravel.com on how to template it (either there is no documentation, or I'm overlooking it)
I know I can edit the 404 page in errors/404.blade.php, but I can't work out how to customize the 403 page, which I've tried to add a custom 403.blade.php page, which doesn't get displayed. ( https://mattstauffer.co/blog/laravel-5.0-custom-error-pages )
Is placing these permission checks in the Request a good idea? Or am I missing something?
Update
I ran a backtrace from authorize(), and it looks like it throws an UnauthorizedException, which extends RuntimeException. I've tried catching both in the routes.php file, which doesn't work either.
I've also tried to create middleware, and call the middleware from a method, which doesn't work either, since the middleware's not even called at all.
Update 2
Ok, so I found out that I can only call $this->middleware() from the constructor, not individual methods, which is progress, I guess.
What i do is add a forbiddenResponse() method to Request abstract class. You can return a response object from that method to render a human readable error.
namespace App\Http\Requests;
use Illuminate\Foundation\Http\FormRequest;
use Illuminate\Http\JsonResponse;
abstract class Request extends FormRequest {
public function forbiddenResponse()
{
return new JsonResponse('Unauthorized', 403);
// or return Response::make('Unauthorized', 403);
}
}
Check the app\Exceptions\Handler.php file. That's where you can customize your exception handling.
The 403 error launches a HttpException. By default, Laravel will look under your resources\views\errors\ directory and try to find a view that corresponds to the same status code. Since you already said that you've created a file called 403.blade.php inside that folder, it should render this page for 403 errors.
One last thing, remember to check inside your web server config file (httpd.conf for Apache, sites-available\your-host for Nginx), if you have a default behavior for any error. If you're using Homestead, you can check the Nginx config file for anything like error_page 404 /index.php;, comment the line and restart the service. That's not the ideal scenario but usually works.
Override the method within your form request object
class CreateUserRequest extends FormRequest {
public function forbiddenResponse(){
return abort(403);
}
}
I have the following (basic) route set up in a CI-based web app:
$route['sms/resend/(:num)/(:any)'] = 'sms/resend/$1/$2';
The controller + 'resend' method:
class Sms extends CI_Controller {
public function resend($to, $message) {
// my code
}
}
Logically speaking, anything that doesn't fit the route should be directed to a 404 page instead of the resend() method within the sms controller. This isn't the case, however. The following URL, for example, isn't redirected correctly, it goes to the same controller+method:
http://myapp/sms/resend/uuuu/WhateverMessage
What could be the problem?
After a bit of digging, I've come to understand that CI's default routing does not get deactivated when a default route related to a specific controller/method pair is added. That being said, if a URL does not fit the route $route['sms/resend/(:num)/(:any)'] = 'sms/resend/$1/$2', then the same URL is run through CI's default routing mechanism as a fallback, so it still takes me to the resend method of the sms controller. To prevent this from happening, I needed to add another custom route that follows all others related to the sms resending, that redirects any other url to a different controller+method. If this controller doesn't exist, you get the default 404 page.
So the final /config/routes.php file:
$route['sms/resend/(:num)/(:any)'] = 'sms/resend/$1/$2';
$route['sms/checkoperator/(:num)'] = 'sms/checkoperator/$1';
$route['sms/(:any)'] = 'somewhereovertherainbow';
I think the rout file is just for rerouting. Your URL don't fits the routing Conditions so it don't gets rerouted! So it goes the normal way wich is the same (in this case!)
Something like this could work!
(! :num) /(:any) '] = error page (or not existing page)
So every request wich doesn't start with a number gets redirected to the error page!
The syntax might be wrong!
This would work great :
$route['sms/resend/[^0-9]/(:any)'] = 'errorpage';
You have to replace the error page by something ;)