Symfony 2 - onSecurityAuthenticationSuccess handler gets called on every page load - php

I have created a security.authentication.success event listener, which should send a line to the logs upon login success. Now every time I load a page which is behind a firewall, I get a successful login message in my logs. If I tried to use
if ($this->container->get('security.context')->isGranted('IS_AUTHENTICATED_FULLY'))
{
$logger->info('Successful login by ' . $username);
}
I get into a recursive madness (xdebug complaining after 10000 nested calls, or whatever high I set it to).
Is there a way to check if the user has just logged in, or if (s)he is using an active session?
Note: I'm using Symfony 2.2 (dev-master)

You have to use the security.interactive_login:
namespace Acme\UserBundle\Listener;
use Symfony\Component\EventDispatcher\Event;
use Symfony\Component\Security\Core\SecurityContext;
use Doctrine\Bundle\DoctrineBundle\Registry as Doctrine; // for Symfony 2.1.x
// use Symfony\Bundle\DoctrineBundle\Registry as Doctrine; // for Symfony 2.0.x
/**
* Custom login listener.
*/
class LoginListener
{
/** #var \Symfony\Component\Security\Core\SecurityContext */
private $securityContext;
/** #var \Doctrine\ORM\EntityManager */
private $em;
/**
* Constructor
*
* #param SecurityContext $securityContext
* #param Doctrine $doctrine
*/
public function __construct(SecurityContext $securityContext, Doctrine $doctrine)
{
$this->securityContext = $securityContext;
$this->em = $doctrine->getEntityManager();
}
/**
* Do the magic.
*
* #param Event $event
*/
public function onSecurityInteractiveLogin(Event $event)
{
if ($this->securityContext->isGranted('IS_AUTHENTICATED_FULLY')) {
// user has just logged in
}
if ($this->securityContext->isGranted('IS_AUTHENTICATED_REMEMBERED')) {
// user has logged in using remember_me cookie
}
// do some other magic here
$user = $this->securityContext->getToken()->getUser();
// ...
}
}

From the documentation:
The security.interactive_login event is triggered after a user has
actively logged into your website. It is important to distinguish this
action from non-interactive authentication methods, such as:
authentication based on a "remember me" cookie.
authentication based on your session.
authentication using a HTTP basic or HTTP digest header.
You could listen on the security.interactive_login event, for example,
in order to give your user a welcome flash message every time they log
in.
The security.switch_user event is triggered every time you activate
the switch_user firewall listener.
http://symfony.com/doc/current/components/security/authentication.html#security-events

Related

Controller check in advance if user can access a route + parameters

I've got a notification system where a route name is stored in the DB along with some a list of parameters used to build a URL. When a user clicks on a notification, these values are recalled and are passed into the router to generate a URL for the user to be redirected to.
A notification has the following structure (simplified structure, there is some inheritance at play here)
class Notification {
/** #var int */
protected $id;
/** #var Uid */
protected $uid;
/** #var string */
protected $title;
/** #var string */
protected $routeName;
/** #var array|null */
protected $routeParameters;
/** #var DateTime */
protected $date;
/** #var DateTime|null */
protected $read;
}
However instead of blindly passing these parameters into the clickable object on screen, they pass through the a redirectAction() defined in the NotificationController.
{{ notification.title }}
This so that I can do a couple things.
Mark the notification as read
Make sure that the user who clicked the notification is the user who the notification was delivered to (otherwise throw an AccessDeniedException)
Make sure that old links are handled nicely <-- the issue
So far, there is a section in my controller that reads the following:
/**
* #Route("/notification/action", name="notification_action")
* #param Request $request
* #param RouterInterface $router
* #return RedirectResponse|Response
*/
public function redirectAction(Request $request, RouterInterface $router) {
// * $notification gets recalled from the DB from the 'uid' in the request params
// * Validation is applied to the notification (check if exists, check if for the correct user)
// * Notification is marked as read with the current timestamp
try {
$url = $router->generate($notification->getRoute(), $notification->getParameters() ?? []);
} catch(RouteNotFoundException | MissingMandatoryParametersException | InvalidParameterException $e) {
return $this->render('notification/broken_action.html.twig');
}
return $this->redirect($url);
}
This is in order to attempt to generate a route and nicely handle a route that either
a) doesn't exist
b) no longer works with older parameters.
This works great if I were to change the definition of a route in the future, but it doesn't handle checking if the user has permission to access the route. This includes:
Checking if the user has the appropriate role to access the route (defined in security.yaml)
Checking if they user is denied access by either the #Security annotation or a voter
Validating parameters interpreted by the the #ParamConverter (such as when an entity may not exist anymore causing a 404)
Handling a thrown exception such as AccessDeniedException or NotFoundHttpException from logic within the action itself
I'm having trouble coming up with a way that I can validate these things before redirecting the user to the $url.
So far all I've come up with is performing a dummy request and seeing if that works before then redirecting the user, but that is yuck for obvious reasons (double request will have extended load times, potentially triggering an action in the background, etc.)
Is there a way that I can check for these things in an efficient way before sending the user to the generated URL? I'm happy to let logic within the action itself slide through validation, as most of the time I tend to avoid this in favour for voters or use of the #Security and #ParamConverter annotations instead. It's just a nice to have for edge cases.
...or I'm open to other ideas on better ways to handle clicking on a notification!

How to set the locale with symfony 3.4

How can I change the locale using symfony 3.4 (php)?
I have the locale saved for each user in my database. Now on this page they explain that you should create an event listener to set the locale. But they only provide a method - in which class do I put this method and how do I wire it up using my services.yml?
And if I'm in the service - how can I access my user object to actually get the locale I want to set?
Here is an example provided by the docs on how to create an kernel request listener.
In this listener you inject the TokenStorage serivce, which then provides you the current token and with it the attached user that is currently logged in. Then you take the locale from the user and set it to the request.
use Symfony\Component\Security\Core\Authentication\Token\Storage\TokenStorageInterface;
class RequestListener
{
private $tokenStorage;
public function __construct(TokenStorageInterface $tokenStorage)
{
$this->tokenStorage = $tokenStorage;
}
public function onKernelRequest(GetResponseEvent $event)
{
$user = $this->tokenStorage->getToken()->getUser();
$request = $event->getRequest();
$request->setLocale($user->getLocale());
}
}
To understand, why Symfony requires the type-hint of an interface instead of an class please read the documentation.

Laravel 5.5 Log in user using event listener

So, I'm trying to log in a user on Laravel 5.5 using SSO. I'm using a library and it works fine, data come back correctly. However when on Listener I try to Auth::login or any Session data, it's not saved. Sessions works fine from any other point of the app, but not in Listener Events.
<?php
namespace App\Listeners;
use App\User;
use Illuminate\Support\Facades\Auth;
use \Aacotroneo\Saml2\Events\Saml2LoginEvent;
use Illuminate\Queue\InteractsWithQueue;
use Illuminate\Contracts\Queue\ShouldQueue;
class AuthListener
{
/**
* Create the event listener.
*
* #return void
*/
public function __construct()
{
//
}
/**
* Handle the event.
*
* #param object $event
* #return void
*/
public function handle(Saml2LoginEvent $event)
{
$messageId = $event->getSaml2Auth()->getLastMessageId();
// your own code preventing reuse of a $messageId to stop replay attacks
$user = $event->getSaml2User();
$attributes = $user->getAttributes();
$id = $attributes['id'];
$laravelUser = User::where('id', $id)->first();
\Session::push('name', 'test');
Auth::login($laravelUser, true);
}
}
Now, at the Auth::login($laravelUser, true) the user is logged in correctly, I did check. But as soon as the page refresh sending to the final URL the session is gone.
Any Ideas?
I was facing the exact problem in Laravel 5.5. It seems that you are missing the routesMiddleware setting in saml2_settings.php file. The description says you need a group which includes StartSession, so you may want to put 'web' there, which has it included by default, as seen in Kernel.php

ZF2 - Assuming other user's identity

Before I dive into reinventing the wheel, I'd first like to check if ZF2 supports, either out-of-the-box or with a 3rd party library, this particular use case where admins log in as other user, or assume their identity.
If not, as I'm not familiar with ZF2 internal design, how would I go into implementing this, with the only constraint being that the system is already built, so I can't change components (controllers, auth services, etc) into supporting it.
My first thought would be to make a mechanism to switch the logged user information stored in the session storage, with the one whose identity I want to assume. Then, write to the session, under a different namespace, the original user information (admin) so that it can be reverted.
Going by this approach, I am expecting components like Zend\Authentication\AuthenticationService return the user whose identity I'm assuming. So, in every call I make to $this->identity()->getId() (identity being a controller plugin for AuthenticationService, that returns the User) in other controllers, the business logic will work normally.
Having said this, the questions would be:
Is there a solution already for this?
Is my approach correct in assuming that by overwriting the session storage I can assume other user ID and expect ZF2 components to work accordingly, or is there any considerations regarding ZF2 internal design/infrastructure I haven't taken in consideration that I should?
Maybe there's a better way to do this?
I think you would need to create your own AuthenticationAdaptor.
class AdminUserLoginAsUser implements \Zend\Authentication\Adapter\AdapterInterface
{
/**
* #var User
*/
private $userToLoginAs;
/**
* #var AdminUser
*/
private $adminUser;
public function __construct(User $userToLoginAs, AdminUser $adminUser)
{
$this->userToLoginAs = $userToLoginAs;
$this->adminUser = $adminUser;
}
/**
* Performs an authentication attempt
*
* #return \Zend\Authentication\Result
* #throws \Zend\Authentication\Adapter\Exception\ExceptionInterface If authentication cannot be performed
*/
public function authenticate()
{
return new \Zend\Authentication\Result(
Result::SUCCESS, $this->user, [
'You have assumed control of user.',
]
);
}
}
The above class will allow you to login as another user when used with Zend's AuthenticationService class.
You will need some way of using Zend's AuthenticationService class and I would recommend using an AuthManager that wraps around the AuthenticationService.
/**
* The AuthManager service is responsible for user's login/logout and simple access
* filtering. The access filtering feature checks whether the current visitor
* is allowed to see the given page or not.
*/
class AuthManager
{
/**
* Authentication service.
* #var \Zend\Authentication\AuthenticationService
*/
private $authService;
/**
* Session manager.
* #var Zend\Session\SessionManager
*/
private $sessionManager;
/**
* Contents of the 'access_filter' config key.
* #var array
*/
private $config;
/**
* Constructs the service.
*/
public function __construct($authService, $sessionManager, $config)
{
$this->authService = $authService;
$this->sessionManager = $sessionManager;
$this->config = $config;
}
/**
* Performs a login attempt. If $rememberMe argument is true, it forces the session
* to last for one month (otherwise the session expires on one hour).
*/
public function login($email, $password, $rememberMe)
{
// Check if user has already logged in. If so, do not allow to log in
// twice.
if ($this->authService->getIdentity()!=null) {
throw new \Exception('Already logged in');
}
// Authenticate with login/password.
$authAdapter = $this->authService->getAdapter();
$authAdapter->setEmail($email);
$authAdapter->setPassword($password);
$result = $this->authService->authenticate();
// If user wants to "remember him", we will make session to expire in
// one month. By default session expires in 1 hour (as specified in our
// config/global.php file).
if ($result->getCode()==Result::SUCCESS && $rememberMe) {
// Session cookie will expire in 1 month (30 days).
$this->sessionManager->rememberMe(60*60*24*30);
}
return $result;
}
public function loginAsUser($user)
{
// Check if user has already logged in. If so, do not allow to log in
// twice.
if ($this->authService->getIdentity() !== null) {
throw new \Exception('Not logged in.');
}
// First need to logout of current user
$this->authService->clearIdentity();
$authAdapter = $this->authService->setAdapter(new AdminUserLoginAsUser($user, $this->authService->getIdentity()));
return $this->authService->authenticate();
}
/**
* Performs user logout.
*/
public function logout()
{
// Allow to log out only when user is logged in.
if ($this->authService->getIdentity()==null) {
throw new \Exception('The user is not logged in');
}
// Remove identity from session.
$this->authService->clearIdentity();
}
}
To see how to plug it all together I would recommend looking at the following resources:
https://olegkrivtsov.github.io/using-zend-framework-3-book/html/en/User_Management__Authentication_and_Access_Filtering.html
https://github.com/olegkrivtsov/using-zf3-book-samples/tree/master/userdemo/module/User
The resources are for zf3 but I think the authenticating of users and managing authentication is very similar to zf2.

Symfony DI : Circular service reference with Doctrine event subscriber

In order to refactor the code about the ticket notification systems, I created a Doctrine listener:
final class TicketNotificationListener implements EventSubscriber
{
/**
* #var TicketMailer
*/
private $mailer;
/**
* #var TicketSlackSender
*/
private $slackSender;
/**
* #var NotificationManager
*/
private $notificationManager;
/**
* We must wait the flush to send closing notification in order to
* be sure to have the latest message of the ticket.
*
* #var Ticket[]|ArrayCollection
*/
private $closedTickets;
/**
* #param TicketMailer $mailer
* #param TicketSlackSender $slackSender
* #param NotificationManager $notificationManager
*/
public function __construct(TicketMailer $mailer, TicketSlackSender $slackSender, NotificationManager $notificationManager)
{
$this->mailer = $mailer;
$this->slackSender = $slackSender;
$this->notificationManager = $notificationManager;
$this->closedTickets = new ArrayCollection();
}
// Stuff...
}
The goal is to dispatch notifications when a Ticket or a TicketMessage entity is created or updated trough mail, Slack and internal notification, using Doctrine SQL.
I already had a circular dependencies issue with Doctrine, so I injected the entity manager from the event args instead:
class NotificationManager
{
/**
* Must be set instead of extending the EntityManagerDecorator class to avoid circular dependency.
*
* #var EntityManagerInterface
*/
private $entityManager;
/**
* #var NotificationRepository
*/
private $notificationRepository;
/**
* #var RouterInterface
*/
private $router;
/**
* #param RouterInterface $router
*/
public function __construct(RouterInterface $router)
{
$this->router = $router;
}
/**
* #param EntityManagerInterface $entityManager
*/
public function setEntityManager(EntityManagerInterface $entityManager)
{
$this->entityManager = $entityManager;
$this->notificationRepository = $this->entityManager->getRepository('AppBundle:Notification');
}
// Stuff...
}
The manager is injected form the TicketNotificationListener
public function postPersist(LifecycleEventArgs $args)
{
// Must be lazy set from here to avoid circular dependency.
$this->notificationManager->setEntityManager($args->getEntityManager());
$entity = $args->getEntity();
}
The web application is working, but when I try to run a command like doctrine:database:drop for example, I got this:
[Symfony\Component\DependencyInjection\Exception\ServiceCircularReferenceException]
Circular reference detected for service "doctrine.dbal.default_connection", path: "doctrine.dbal.default_connection -> mailer.ticket -> twig -> security.authorization_checker -> security.authentication.manager -> fos_user.user_provider.username_email -> fos_user.user_manager".
But this is concerning vendor services.
How to solve this one? Why I have this error only on cli?
Thanks.
Had the same architectural problem lately, assuming you use Doctrine 2.4+ the best thing to do is not use the EventSubscriber (which triggers for all events), but use EntityListeners on the two entities you mention.
Assuming that the behavior of both entities should be the same, you could even create one listener and configure it for both entities. The annotation looks like this:
/**
* #ORM\Entity()
* #ORM\EntityListeners({"AppBundle\Entity\TicketNotificationListener"})
*/
class TicketMessage
Thereafter you can create the TicketNotificationListener class and let a service definition do the rest:
app.entity.ticket_notification_listener:
class: AppBundle\Entity\TicketNotificationListener
calls:
- [ setDoctrine, ['#doctrine.orm.entity_manager'] ]
- [ setSlackSender, ['#app.your_slack_sender'] ]
tags:
- { name: doctrine.orm.entity_listener }
You might not even need the entity manager here, because the entity itself is available via the postPersist method directly:
/**
* #ORM\PostPersist()
*/
public function postPersist($entity, LifecycleEventArgs $event)
{
$this->slackSender->doSomething($entity);
}
More info on Doctrine entity listeners: http://docs.doctrine-project.org/projects/doctrine-orm/en/latest/reference/events.html#entity-listeners
IMHO you are mixing 2 different concepts here:
Domain Events (TicketWasClosed for example)
Doctrine's Life-cycle Events (PostPersist for example)
Doctrine's event system is meant to hook into the persistence flow, to deal stuff directly related to saving to and loading from the database. It shouldn't be used for anything else.
To me it looks like what you want to happen is:
When a ticket was closed, send a notification.
This has nothing to do with Doctrine or persistence in general. What you need is another event system dedicated to Domain Events.
You can still use the EventManager from Doctrine, but make sure you create a second instance which you use for Domain Events.
You can also use something else. Symfony's EventDispatcher for example. If you're using the Symfony framework, the same thing applies here as well: don't use Symfony's instance, create your own for Domain Events.
Personally I like SimpleBus, which uses objects as events instead of a string (with an object as "arguments"). It also follows the Message Bus and Middleware patterns, which give a lot more options for customization.
PS: There are a lot of really good articles on Domain Events out there. Google is your friend :)
Example
Usually Domain Events are recorded within entities themselves, when performing an action on them. So the Ticket entity would have a method like:
public function close()
{
// insert logic to close ticket here
$this->record(new TicketWasClosed($this->id));
}
This ensures the entities remain fully responsible for their state and behavior, guarding their invariants.
Of course we need a way to get the recorded Domain Events out of the entity:
/** #return object[] */
public function recordedEvents()
{
// return recorded events
}
From here we probably want 2 things:
Collect these events into a single dispatcher/publisher.
Only dispatch/publish these events after a successful transaction.
With the Doctrine ORM you can subscribe a listener to Doctrine's OnFlush event, that will call recordedEvents() on all entities that are flushed (to collect the Domain Events), and PostFlush that can pass those to a dispatcher/publisher (only when successful).
SimpleBus provides a DoctrineORMBridge that supplies this functionality.

Categories