Zend Framework 2 - Check for authentication in abstract controller -> onDispatch? - php

I am re-writing the authentication process for my application running under ZF2.
I need to have more options to authenticate the user depending on the service called.
I.E. for web application accessed from browser I will authenticate via Ldap and for API services I will check the user credentials in the header.
I created an abstract controller checking if the user is authenticated; if not it will be redirected to login page.
All the controllers in the modules needing the same authentication process will extend this class.
I need to save the original request to redirect it back to it after successful login.
My questions are:
1. Is the abstract controller -> onDispatch() method the right
place to place it?
Every solution I found around was always doing it in the Module.php. To distinguish the auth method they need to check if the requested controller match, since Module.php is called always. Isn't it 'cleaner' to set it in the controller?
2. Should I use redirect or forward to pass from original controller
to login controller and then back?
I don't mind the url changing in the browser bar, just looking for the best and fastest solution keeping also original request.
3. Is it correct to store the uri in the session class ( from the
auth module)? Is there any way to conserve the whole request (including maybe the POST data in case needed)?
Here is the abstract controller:
abstract class AbstractAuthActionController extends AbstractActionController {
public function onDispatch(MvcEvent $e) {
$serviceManager = $e->getApplication ()->getServiceManager ();
$auth = $serviceManager->get ( 'LdapAuth\Client\Ldap' );
if (! $auth->hasIdentity ()) {
$uri = $e->getRequest()->getRequestUri();
$callBackFunction = $this->getLdap ()->getCallBackFunction (); // = new SessionData();
$callBackFunction::setOriginalUri($uri); // function to store temporarly the uri
return $this->redirect ()->toRoute ( 'ldap-login-route' );
} else {
return parent::onDispatch ( $e );
}
}
}

A lot of people do that because they want to take care of checking authentication before the controller dispatch event. Authentication can be checked much earlier in the process, for example on route event, or at least before the controller has been instantiated (dispatch with higher priority then controller).
In case the user is unauthenticated you want to respond with a 401 (unauthorized) or 403 (forbidden) or a 302 (moved temporarily) response (read some more background on this status code here on Wiki) as early as possible to prevent all the overhead which only keeps your server (unnecessarily) occupied and thus slows down your application and delays the unauthenticated response.
module.php is NOT the best place to add all the authentication related code. Better would be to create an authentication listener (and inject a authentication service in the listener) and only connect the listener in your module.php.
Read on the difference between redirect and forward here in this answer. If want to redirect the client that it is not properly authenticated in a response with a 302 status code you will need to send a redirect response including this status code. I also see people using forward in such cases, but in my opinion it is not correct, because the client won't get notified about any redirection. You could also check authentication modules like ZfcUser to see how they handle this.
You don't need to store this url on the server, you can send the url you want to go to after logging in (the original url) inside the redirect response. For example you redirect to login.php from a request targeted at profile.php, then your redirect url could look like this:
http://www.example.com/login.php?redirect=profile.php
You can now set the redirect inside the login process/controller so that after a successful login you return the client to to profile.php.

Related

Custom 404 error template in twig 2.5 with symfony 4.1

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')]);
// ...
}

Yii2: Execute a redirect from static non-controller context

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.

Yii2 REST API Basic Auth (SESSION OVERRIDE)

I have implemented findIdentityByAccessToken in my Users model.
public static function findIdentityByAccessToken($token, $type = null)
{
$apiUser = ApiAccess::find()
->where(['access_token' => $token])
->one();
return self::findOne(['id' => $apiUser->idUser]);
}
In the browser, if i'm logged into the system, I can hit an api get endpoint, enter my auth token and be authenticated properly.
However, If i am not logged in, I get kicked back to my login screen. Using a rest client, I am returned the HTML of the login screen.
This indicates 1 of 2 things in my eyes. Either, in the current state, it is requiring a 'logged in session' in order to access that api module. Or #2, I'm not properly passing the auth token.
My Request header:
Accept: */*
Cache-Control: no-cache
Authentication: Basic base64('mytoken':)
How do I override my "default" login behavior? OR Properly send the authentication token?
You can override login method and loginByAccessToken from model User to change the login behavior. See: http://www.yiiframework.com/doc-2.0/yii-web-user.html
On the other hand, what you probably need (in case that you don't have it yet) is to write a controller and implement a login action. Then implement a class extending from AuthMethod and authenticate method (and maybe challenge method). After that you can add that class as a behavior to all your controllers (or even better make all your controller inherit from one controller with that behavior).
Plase take a look at this link: http://www.yiiframework.com/doc-2.0/guide-rest-authentication.html

Symfony routing for a REST API / Single page app

I'm trying to setup routes for my Symfony2 single page app and I'm not sure how to go about it correctly without it feeling super hacky.
Here is what I need it to do and how I've attempted to set it up:
When Authenticated
Any route requesting application/jsonshould hit the routes they have been setup for as usual.
Any route that is entered not requesting application/json should load a controller that renders a twig file containing all the JS for my single page app.
Any static resource that doesn't exist and ends looking for a symfony route eg [.js, .css, .jpeg, etc] should return 404.
When NOT Authenticated
Anything requesting application/json should return 403
Anything NOT requesting application/json should return to the login page
Here is what i've attempted so far:
Setup routes with the FOSRestBundle for each service
Setup a listener that returns the base controller html if the request isn't application/json
if (!in_array('application/json', $request->getAcceptableContentTypes())) {
$fakeRequest = $event->getRequest()->duplicate(
null,
null,
array('_controller' => 'HvHDashboardBundle:Dashboard:index')
);
$controller = $this->resolver->getController($fakeRequest);
$event->setController($controller);
}
Setup a bunch of 'catch all' routes to fake a 404 if the static resource doesn't exist.
# routing.yml
# Catch any files that are meant to be their own static resource and return 404
catch_all_fail:
pattern: /{uri}.{_format}
defaults: { _controller: MyBundle:Dashboard:return404 }
requirements:
_format: js|hbs|css|jpg|gif|jpeg|png
Issues
This approach feels like a massive hack, and is not how the Symfony routing system is intended to work
The base controller page is returned even if you aren't authenticated because the type listener is being hit before the security context and forcing that controller to render.
Question:
How do other solve this issue with routing and single page apps with Symfony where they initially need to render HTML with twig, then JS takes over and requests JSON?
Only make an API, no static pages at all. Trust me, I recently did a moderate sized API with Symfony and that's the way to go. It will simplify your backend security a lot, if you do not mix the API with static pages. Ofcourse you can still have static pages, if you want to have some sort of Landing page or something. but try not to mix them with the main app.
Even for login, do not make a static page, but instead have an api route that will validate username/password and return the user the auth token in response. One user can have multiple tokens for example (can be logged in at multiple locations), and the token is sent in the request headers everytime.
If the token is validated ok, symfony will know which user it belongs, so you will know the 'User'. If no token is present, it should return "Not Authenticated", and if token is invalid also something like that or 'Bad request'.
One thing that I had to do when doing with APIs is that I had to write a request listener to accept JSON content and transform it into request object, so I could access data with $request->request.
If you have any questions, let me know in comment and I can help.
As far as routing is concerned, follow the REST rules and you will be good to go.

Event dispatch in Magento before redirect

I'm running into an issue when using event dispatch in Magento.
I'm using controller_action_predispatch to set a frontend session variable from a parameter in the URL.
Now, the issue seems like, when the user comes to the site initially, they might lend on a page that will redirect them to base URL (such a example.com to www.example.com).
But for some reason, after redirect, the session variable is lost...
Any ideas?
Thank you.
EDIT:
adding the code used:
public function grabRef($observer) {
$ref = Mage::app()->getRequest()->getParam('ref', $default);
if (isset($ref) && !is_null($ref) and !empty($ref)) {
Mage::getSingleton('core/session',array('name'=>'frontend'))->setRefid($ref);
}
}
There are only two remotely useful events dispatched prior to this redirection, but they are not specific to the redirect:
controller_front_init_before
controller_front_init_routers
The redirect depends on the "Auto-redirect to Base URL" setting from System > Configuration > Web > Url Options, which is evaluated by Mage_Core_Controller_Varien_Front->_checkBaseUrl(). This redirect occurs before any dispatching takes place, and it does not append GET or POST data, hence the loss of the param you are trying to capture.
Normally sessions are initialized under adminhtml or frontend session namespace based on the controller class being used (ref the action controller superclass method Mage_Core_Controller_Varien_Action->preDispatch(). You should be able to move your observer configuration under global/events/controller_front_init_before. Note that you must do this in the global event area, as the frontend event configuration part does not load until after this event is dispatched. That particular scenario cost me an hour once!

Categories