Symfony2: Check user authentication based on path - php

in Symfony2, is it possible to check if user is authenticated to access the URl he requested.
What I want to do is, i dont want to allow a logged in user to go back to registration or login or recover password pages.
here is my security.yml:
access_control:
- { path: ^/signup/, roles: IS_AUTHENTICATED_ANONYMOUSLY && !IS_AUTHENTICATED_FULLY}
- { path: ^/register/, roles: IS_AUTHENTICATED_ANONYMOUSLY && !IS_AUTHENTICATED_FULLY}
- { path: ^/recover/, roles: IS_AUTHENTICATED_ANONYMOUSLY && !IS_AUTHENTICATED_FULLY}
but this is showing, access denied page to current user. So i think it would be nice if I can redirect the user to home page on such request, by checking if he is not allowed. Can I check by providing path that user is authenticated or not in listener?
public function onKernelResponse(FilterResponseEvent $event)
{
$request = $event->getRequest();
$path = $request->getPathInfo();
if($this->container->get('security.context')->getToken() != null) {
// To check if user is authenticated or anonymous
if( ($this->container->get('security.context')->getToken() instanceof UsernamePasswordToken) &&
($this->container->get('security.context')->isGranted('IS_AUTHENTICATED_FULLY') == true) ) {
// HOW TO CHECK PATH ?
// set response to redirect to home page
}
}
}

The security.access_map service
The configuration of security.access_control is processed by ...
SecurityBundle\DependencyInjection\SecurityExtension
... which creates RequestMatchers for the routes (path,hosts,ip,...) and then invokes the service's add() method with the matcher, the allowed roles and the channel (i.e. https ).
The service is usually used by i.e. the AccessListener.
You can use the security.access_map service to access the
security.access_control parameters in your application.
The class used for the security.access_map service is defined by the parameter security.access_map.class and defaults to
Symfony\Component\Security\Http\AccessMap ( implements
AccessMapInterface )
You can use the parameter security.access_map.class to override the service with a custom class (must implement AccessMapInterface):
# i.e. app/config/config.yml
parameters:
security.access_map.class: My\Custom\AccessMap
How to access the service
The security.access_map service is a private service as you can see by it's definition here.
This means you can't request it from the container directly like this:
$this->container->get('security.access_map')
You will have to inject it into another service (i.e. a listener service) explicitly to be able to access it.
A listener example
services:
my_listener:
class: My\Bundle\MyListenerBundle\EventListener\ForbiddenRouteListener
arguments: [ #security.access_map ]
tags:
- { name: kernel.event_listener, event: kernel.request, method: onKernelRequest }
Then you can call the getPatterns() method to obtain the RequestMatchers, allowed roles and required channel from there.
namespace My\Bundle\MyListenerBundle\EventListener;
use Symfony\Component\Security\Http\AccessMapInterface;
use Symfony\Component\HttpKernel\Event\GetResponseEvent;
class ForbiddenRouteListener
{
protected $accessMap;
public function __construct(AccessMapInterface $access_map)
{
$this->accessMap = $access_map;
}
public function onKernelRequest(GetResponseEvent $event)
{
$request = $event->getRequest();
$patterns = $this->accessMap->getPatterns($request);
// ...

Maybe this will help someone. I just catch route name and check if they are in array. If yes just redirect. This is event listener.
services.yml
project.loggedin_listener:
class: Project\FrontBundle\EventListener\LoggedInListener
arguments: [ "#router", "#service_container" ]
tags:
- { name: kernel.event_listener, event: kernel.request, method: onKernelRequest }
listener:
namespace Project\FrontBundle\EventListener;
use Symfony\Component\HttpKernel\Event\GetResponseEvent;
use Symfony\Component\HttpFoundation\RedirectResponse;
class LoggedInListener {
private $router;
private $container;
public function __construct($router, $container)
{
$this->router = $router;
$this->container = $container;
}
public function onKernelRequest(GetResponseEvent $event)
{
$container = $this->container;
$accountRouteName = "_homepage";
if( $container->get('security.context')->isGranted('IS_AUTHENTICATED_FULLY') ){
$routeName = $container->get('request')->get('_route');
$routes = array("admin_login","fos_user_security_login","fos_user_registration_register","fos_user_resetting_request");
if(in_array($routeName, $routes)){
$url = $this->router->generate($accountRouteName);
$event->setResponse(new RedirectResponse($url));
}
}
}
}

You can do not only inside security.yml options, but also via controller, like this:
if($securityContext->isGranted('IS_AUTHENTICATED_REMEMBERED')) {
return $this->redirect($this->generateUrl('homepage'));
}

Related

How to get firewall name from Request in Symfony 5?

The question is simple. I am implmenting AccessDeniedListener and I get an ExceptionEvent object. From this I can get request. I want to apply certain logic ONLY if I am inside one of my firewalls defined in security.yaml.
How can I get the Firewall name from ExceptionEvent or Request instances?
EDIT:
I have found this code "works"
$firewall_context_name = $event->getRequest()->attributes->get('_firewall_context');
However Im not very happy about it. There should be a FirewallContext or FirewallConfig objects retrieveable somehow, no?
Thanks
class AccessDeniedListener implements EventSubscriberInterface
{
public static function getSubscribedEvents(): array
{
return [
// the priority must be greater than the Security HTTP
// ExceptionListener, to make sure it's called before
// the default exception listener
KernelEvents::EXCEPTION => ['onKernelException', 2],
];
}
public function onKernelException(ExceptionEvent $event): void
{
$exception = $event->getThrowable();
if (!$exception instanceof AccessDeniedException) {
return;
}
$request = $event->getRequest();
// HOW TO GET FIREWALL NAME HERE???
security.yaml
firewalls:
dev:
pattern: ^/(_(profiler|wdt)|css|images|js)/
security: false
api:
pattern: ^/api/
security: false
main:
custom_authenticators:
- App\Security\LoginFormAuthenticator
logout:
path: app_logout
lazy: true
provider: app_user_provider
As stated in the docs you linked:
This object can be accessed through the getFirewallConfig(Request $request) method of the
Symfony\Bundle\SecurityBundle\Security\FirewallMap class
This class cannot be injected directly, so you'll have to configure your dependency in services.yaml using the service alias security.firewall.map (or create a service alias if you plan to use it somewhere else).
services:
# ...
App\Listener\AccessDeniedListener:
arguments:
$firewallMap: '#security.firewall.map'
Now modify your listener to receive this parameter:
class AccessDeniedListener implements EventSubscriberInterface
{
private $firewallMap;
public function __construct(FirewallMapInterface $firewallMap)
{
$this->firewallMap = $firewallMap;
}
// Ommited getSubscribedEvents
public function onKernelException(ExceptionEvent $event): void
{
$request = $event->getRequest();
$firewallConfig = $this->firewallMap->getFirewallConfig($request);
if (null === $firewallConfig) {
return;
}
$firewallName = $firewallConfig->getName();
}
}

Call an route on each page

I have a question. I added a new service PopupListener.php:
use Symfony\Bundle\FrameworkBundle\Routing\Router;
use Symfony\Component\DependencyInjection\ContainerInterface;
class PopupListener
{
protected $router;
public function __construct(Router $router)
{
$this->router = $router;
}
public function onKernelRequest()
{
$this->router->generate('app_popup_trigger');
}
}
services.yml :
popup_listener:
class: App\DesktopBundle\Listeners\PopupListener
arguments: ["#router"]
tags:
- { name: kernel.event_listener, event: kernel.request, method: onKernelRequest }
routing.yml :
app_popup_trigger:
path: /popup/trigger
defaults: { _controller: AppDesktopBundle:Popup:triggerPopup }
The triggerPopupAction :
class PopupController extends Controller{
public function triggerPopupAction(){
return $this->render('AppDesktopBundle:Popup:index.html.twig', array());
}
}
I want that at each route call the new route added : app_popup_trigger. I made somethink like that but not work. The route is not called. Can you help me please ?
Basically use FOSJsRoutingBundle and trigger your route with javascript. That will be easier than listeners for a popup.
To call a specific route at the start of every request, you just need to extend your code in your PopupListener:
use Symfony\Bundle\FrameworkBundle\Routing\Router;
use Symfony\Component\DependencyInjection\ContainerInterface;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpKernel\Event\GetResponseEvent;
use Symfony\Component\HttpKernel\HttpKernelInterface;
class PopupListener
{
protected $router;
protected $httpKernel;
public function __construct(Router $router, HttpKernelInterface $httpKernel)
{
$this->router = $router;
$this->httpKernel = $httpKernel;
}
public function onKernelRequest(GetResponseEvent $event)
{
if (HttpKernelInterface::MASTER_REQUEST !== $event->getRequestType()) {
return;
}
$subRequest = Request::create($this->router->generate('app_popup_trigger'));
$response = $this->httpKernel->handle($subRequest, HttpKernelInterface::SUB_REQUEST);
// do something with the $response here
}
}
Symfony will start a new request-response cycle just for this sub-request and will return the $response of this cycle. Then you have to decide what you are doing with that reponse.
And then add the additional service to your service configuration:
popup_listener:
class: App\DesktopBundle\Listeners\PopupListener
arguments: ["#router", "#http_kernel"]
tags:
- { name: kernel.event_listener, event: kernel.request, method: onKernelRequest }
You can get more information about symfony sub-requests here: The HttpKernel Component - Sub Request. I linked the documentation for Symfony 2.3. But keep in mind Symfony 2.3 is not maintained anymore and you should consider upgrading to 3.x.

Showing current user in log

I'm trying to create a custom monolog processor to attach the current user to an error mailer.
When declaring a service like so:
monolog.processor.mail:
class: MyVendor\Monolog\Processor\MailProcessor
arguments:
- #mailer
- #security.context
tags:
- { name: monolog.processor, method: processRecord }
I get a circular reference:
[Symfony\Component\DependencyInjection\Exception\ServiceCircularReferenceException]
Circular reference detected for service "monolog.processor.mail",
path: "router -> monolog.logger.router -> monolog.processor.mail
-> security.context -> security.authentication.manager
-> fos_user.user_provider.username_email-> fos_user.user_manager
-> doctrine.orm.default_entity_manager -> doctrine.dbal.default_connection
-> doctrine.dbal.logger -> monolog.logger.doctrine".
What would be the best practice solution here?
A related forum thread:
http://forum.symfony-project.org/viewtopic.php?t=40306&p=131081#p131143
This thread shows that:
Setter injection doesn't solve the issue (i tried this as well)
Injecting the container causes an infinitive recursion (this i have not confirmed)
Also tried this script http://pastebin.com/AuvFgTY3 to get the user from the session.
if ($this->session !== null) {
if ($this->session->has($this->securityKey)) {
$token = unserialize($this->session->get($this->securityKey));
$this->currentUser = $token->getUser();
}
}
This gave the following error:
Warning: ini_set(): A session is active. You cannot change the session module's ini settings at this time in
C:\inetpub\symfony23\vendor\symfony\symfony\src\Symfony\Component\HttpFoundation\Session\Storage\Handler\NativeFileSessionHandler.php
on line 56
I do understand that the security.context has not yet been build for services which request the logger very early on. For my class it's not a problem since i will set the user to undefined. So ideally the security.context would be setter injected AFTER the security.context service has been created. However i can not change the priority on the logger to be constructed very late because it's needed early on.
So maybe the question resolves to: how to recreate the service again after security.context has been initialized? Not sure if scope prototype would help here??
Create handler on kernel request and extract user:
// src/AppBundle/Monolog/UserProcessor.php
namespace AppBundle\Monolog;
use Symfony\Component\HttpKernel\Event\GetResponseEvent;
use Symfony\Component\Security\Core\Authentication\Token\Storage\TokenStorageInterface;
class UserProcessor
{
private $tokenStorage;
private $user;
public function __construct(TokenStorageInterface $tokenStorage)
{
$this->tokenStorage = $tokenStorage;
}
public function __invoke(array $record)
{
if (null !== $this->user) {
$record['extra']['user'] = $this->user->getUsername();
}
return $record;
}
public function onKernelRequest(GetResponseEvent $event)
{
if (null === $token = $this->tokenStorage->getToken()) {
return;
}
if (!is_object($user = $token->getUser())) {
// e.g. anonymous authentication
return;
}
$this->user = $user;
}
}
Register new processor:
# app/config/services.yml
services:
monolog.processor.user:
class: AppBundle\Monolog\UserProcessor
arguments: ["#security.token_storage"]
tags:
- { name: monolog.processor }
- { name: kernel.event_listener, event: kernel.request, method: onKernelRequest }
Symfony Documentation has problem

Change language on login

Symfony 2.1.3-dev
SonataUserBundle
SonataAdminBundle
JMSI18nRoutingBundle
By default the language is french, but I enabled "en"
I installed this bundles and most things work fine.
But I would like to do the following :
A user XXX (SonataUserBundle) has in the field "locale" the value "en"
When this user logs in I want to show up the pages in english.
The user has not to switch manually.
I think this should be done on the autentification process.
The problem is that SonataUserBundle (based on FOSUser) does not do the authentification (seen HERE)
So I tried to do THIS, but there must be some configuration issues.
When applying the wsse_secured to the whole site :
wsse_secured:
pattern: ^/
wsse: true
I get the following error : A Token was not found in the SecurityContext.
When adding anonymous to my config.yml :
firewalls:
....
main:
pattern: ^/
wsse: true
anonymous: true
I can access the home page, but when trying to login I get this error :
You must configure the check path to be handled by the firewall using form_login in your security firewall configuration.
When adding the checkpath for FOS it works but the systems does not work with my wsse-provider (I added code in WsseProvider.php to make me know)
So my question : How can I get work this WSSE authentification. I followed strictly the indications in the doc.
EDIT :
I perhaps made an error by implementing the wsse security files in my own bundle.
Now I moved it to sonata user bundle and I get the following error :
ErrorException: Catchable Fatal Error: Argument 1 passed to Application\Sonata\UserBundle\Security\Authentication\Provider\WsseProvider::__construct() must implement interface Symfony\Component\Security\Core\User\UserProviderInterface, string given, called in ..\app\cache\dev\appDevDebugProjectContainer.php on line 4413 and defined in ..\src\Application\Sonata\UserBundle\Security\Authentication\Provider\WsseProvider.php line 17
What's wrong with my UserProviderInterface in WsseProvider.php :
<?php
namespace Application\Sonata\UserBundle\Security\Authentication\Provider;
use Symfony\Component\Security\Core\Authentication\Provider\AuthenticationProviderInterface;
use Symfony\Component\Security\Core\User\UserProviderInterface;
use Symfony\Component\Security\Core\Exception\AuthenticationException;
use Symfony\Component\Security\Core\Exception\NonceExpiredException;
use Symfony\Component\Security\Core\Authentication\Token\TokenInterface;
use Application\Sonata\UserBundle\Security\Authentication\Token\WsseUserToken;
class WsseProvider implements AuthenticationProviderInterface
{
private $userProvider;
private $cacheDir;
public function __construct(UserProviderInterface $userProvider, $cacheDir)
{
$this->userProvider = $userProvider;
$this->cacheDir = $cacheDir;
}
...
I've solve this problem with a simple KernelRequestListener:
<?php
namespace ACME\DemoBundle\Listener;
use Symfony\Component\HttpKernel\Event\GetResponseEvent;
use Symfony\Component\DependencyInjection\ContainerInterface;
class RequestListener
{
protected $container;
public function __construct(ContainerInterface $container)
{
$this->container = $container;
}
public function onKernelRequest(GetResponseEvent $event)
{
$userlocale = null;
$request = $event->getRequest();
$user = $this->container->get('security.context')->getToken()->getUser();
if (!is_object($user)) {
return;
}
$userlocale = $user->getLocale();
if($userlocale !== NULL AND $userlocale !== '')
{
$request->setLocale($userlocale);
}
}
}
Register service in Acme/Demo/Resources/service.yml:
ACME.demo.listener.request:
class: ACME\DemoBundle\Listener\RequestListener
arguments: [ #service_container ]
tags:
- { name: kernel.event_listener, event: kernel.request, method: onKernelRequest }
Other solution I've found there: Here

Symfony2: After successful login event, perform set of actions

I need to perform a set of actions after a user successfully logs in. This includes loading data from the database and storing it in the session.
What is the best approach to implementing this?
You can add a listener to the security.interactive_login event.
attach your listener like so. In this example I also pass the security context and session as dependencies.
Note: SecurityContext is deprecated as of Symfony 2.6. Please refer to
http://symfony.com/blog/new-in-symfony-2-6-security-component-improvements
parameters:
# ...
account.security_listener.class: Company\AccountBundle\Listener\SecurityListener
services:
# ...
account.security_listener:
class: %account.security_listener.class%
arguments: ['#security.context', '#session']
tags:
- { name: kernel.event_listener, event: security.interactive_login, method: onSecurityInteractiveLogin }
and in your listener you can store whatever you want on the session. In this case I set the users timezone.
<?php
namespace Company\AccountBundle\Listener;
use Symfony\Component\Security\Core\SecurityContextInterface;
use Symfony\Component\HttpFoundation\Session\Session;
use Symfony\Component\Security\Http\Event\InteractiveLoginEvent;
class SecurityListener
{
public function __construct(SecurityContextInterface $security, Session $session)
{
$this->security = $security;
$this->session = $session;
}
public function onSecurityInteractiveLogin(InteractiveLoginEvent $event)
{
$timezone = $this->security->getToken()->getUser()->getTimezone();
if (empty($timezone)) {
$timezone = 'UTC';
}
$this->session->set('timezone', $timezone);
}
}
You can even fetch the user instance from the event itself, no need to inject the token storage!
public function onSecurityInteractiveLogin(InteractiveLoginEvent $event)
{
$event->getAuthenticationToken()->getUser()
}

Categories