The admin user in my Symfony 4.2 application should be able to log out another (non-admin) user. I created a user login system depending on the Symfony security-bundle (https://symfony.com/doc/current/security/form_login_setup.html).
Now I am building an admin dashboard where all user have to be listed with their online status (last activity).
Is there a recommended way to list active users and kill their session if needed?
I've read some posts like this: Symfony how to return all logged in Active Users. But the answers are a little bit older and are just about listing the active users.
The correct way is to store the user session in the database.
https://symfony.com/doc/current/doctrine/pdo_session_storage.html (in here is the create syntax of the database table. Also add a user_id to the table)
in framework.yml add the Pdo Session Handler.
session:
handler_id: Symfony\Component\HttpFoundation\Session\Storage\Handler\PdoSessionHandler
cookie_secure: auto
cookie_samesite: lax
In service.yml add a listener and register the session handler
# Handlers
Symfony\Component\HttpFoundation\Session\Storage\Handler\PdoSessionHandler:
arguments:
- !service { class: PDO, factory: 'database_connection:getWrappedConnection' }
- { lock_mode: 1 }
# Listeners
App\Listener\SessionListener:
tags:
- {name: kernel.event_listener, event: kernel.request, method: onRequestListener}
create a new listener in
class SessionListener
{
/**
* #var TokenStorageInterface
*/
private $tokenStorage;
/**
* #var EntityManagerInterface
*/
private $em;
/**
* #var SessionInterface
*/
private $session;
public function __construct(
TokenStorageInterface $tokenStorage,
EntityManagerInterface $em,
SessionInterface $session
) {
$this->tokenStorage = $tokenStorage;
$this->em = $em;
$this->session = $session;
}
public function onRequestListener(GetResponseEvent $event): void
{
// If its not te master request or token is null
if (!$event->isMasterRequest() || $this->tokenStorage->getToken() === null) {
return;
}
/** #var User $user */
$user = $this->tokenStorage->getToken()->getUser();
// Check if user is logged in
if (!$user instanceof User) {
return;
}
$connection = $this->em->getConnection();
try {
$stmt = $connection->prepare('UPDATE `sessions` SET `user_id` = :userId WHERE `sess_id` = :sessionId');
$stmt->execute([
'userId' => $user->getId(),
'sessionId' => $this->session->getId(),
]);
} catch (DBALException $e) {
}
}
}
Now just delete the sessions from this user.
/**
* #var EntityManagerInterface
*/
private $em;
public function __construct(EntityManagerInterface $em)
{
$this->em = $em;
}
public function delete(User $user): void
{
$sessions = $this->em->getRepository(Session::class)->findBy([
'user' => $user,
]);
foreach ($sessions as $session) {
$this->em->remove($session);
}
$this->em->flush();
}
Here's a good way to kill user sessions:
use an EventListener with an onKernelRequest event. In your main code: public function onKernelRequest(KernelEvent $event)
$request = $event->getRequest();
$token = $this->container->get('security.token_storage')->getToken();
if ($token === null) { // somehow
return;
}
if ($token->getUser()->isLocked() === true) {
// you must implement a boolean flag on your user Entities, which the admins can set to false
$this->container->get('security.token_storage')->setToken(); // default is null, therefore null
$request->getSession()->invalidate(); // these lines will invalidate user session on next request
return;
}
Now, on to your other question: How to list users with their online status? Easy, your user Entities should implement another boolean flag, such as isOnline (with a getter and setter).
Next, you should create a LoginListener (no need to implement any interface). And in your main code:
public function onSecurityInteractiveLogin(InteractiveLoginEvent $event) {
$user = $event->getAuthenticationToken()->getUser();
if ($user instanceof UserInterface) {
// set isOnline flag === true
// you will need to fetch the $user with the EntityManager ($this->em)
// make sure it exists, set the flag and then
$this->em->flush();
}
}
Your third event should be a LogoutListener, where you will set the isOnline flag === false
Symfony calls a LogoutListener (as a handler) when a user requests logout.
But you can write your own:
class LogoutListener implements LogoutHandlerInterface {
public function logout(Request $request, Response $response, TokenInterface $token): void
{
$user = $token->getUser();
if (!$user instanceof UserInterface) { /** return if user is somehow anonymous
* this should not happen here, unless... reasons */
return;
}
// else
$username = $user->getUsername(); // each user class must implement getUsername()
// get the entity Manager ($this->em, injected in your constructor)
// get your User repository
$repository = $this->em->getRepository(MyUser::class);
$user = $repository->findOneBy(['username' => $username]); // find one by username
$user->setIsOnline(false);
$this->em->flush(); // done, you've recorded a logout
}
}
Hope this helps. With a bit of luck, it will. Cheers! :-)
Related
How to automatically add the currently authorized user to a resource upon creation (POST).
I am using JWT authentication, and /api/ routes are protected from unauthorized users. I want to set it up so that when an authenticated user creates a new resource (i.e. by sending a POST request to /api/articles) the newly created Article resource is related to the authenticated user.
I'm currently using a custom EventSubscriber per resource type to add the user from token storage.
Here's the gist for the subscriber base class:
https://gist.github.com/dsuurlant/5988f90e757b41454ce52050fd502273
And the entity subscriber that extends it:
https://gist.github.com/dsuurlant/a8af7e6922679f45b818ec4ddad36286
However this does not work if for example, the entity constructor requires the user as a parameter.
E.g.
class Book {
public User $owner;
public string $name;
public class __construct(User $user, string $name) {
$this->owner = $user;
$this->name = $name;
}
}
How to automatically inject the authorized user upon entity creation?
For the time being, I'm using DTOs and data transformers.
The main disadvantage is having to create a new DTO for each resource where this behaviour is required.
As a simple example, I'm doing something like this:
class BootDtoTransformer implements DataTransformerInterface
{
private Security $security;
public function __construct(Security $security)
{
$this->security = $security;
}
public function transform($data, string $to, array $context = [])
{
$owner = $this->security->getUser();
return $new Book($owner, $data->name);;
}
public function supportsTransformation($data, string $to, array $context = []): bool
{
if ($data instanceof Book) {
return false;
}
return Book::class === $to && null !== ($context['input']['class'] ?? null);
}
}
This logically works only for a single resource. To have a generic transformer for multiple resources I end up using some interfaces to set apart the "owneable" resources, and a bit of reflection to instantiate each class.
I would have thought this were doable during the denormalization phase, but I couldn't get it work.
As #nealio82 and #lavb said, you should have a look on Gedmo\Blameable which help you to handle properties as createdBy or updatedBy where you can store the User who create the ressource.
Blameable
StofDoctrineExtensionsBundle
Then to handle access, have a look on Voters which is awesome to handle security and different access.
Official Symfony documentation about Voters
e.g
Book entity
...
use Gedmo\Mapping\Annotation as Gedmo;
class Book {
...
/**
* #var string $createdBy
*
* #Gedmo\Blameable(on="create")
* #ORM\Column
*/
public User $owner;
public function getOwner() {
return $this->owner;
}
public function setOwner(User $owner) {
$this->owner = $owner
}
}
src/Security/Voter/BookVoter
namespace App\Security;
use App\Entity\Book;
use App\Entity\User;
use Symfony\Component\Security\Core\Authentication\Token\TokenInterface;
use Symfony\Component\Security\Core\Authorization\Voter\Voter;
class BookVoter extends Voter
{
const VIEW = 'view';
const EDIT = 'edit';
protected function supports(string $attribute, $subject)
{
// if the attribute isn't one we support, return false
if (!in_array($attribute, [self::VIEW, self::EDIT])) {
return false;
}
// only vote on `Book` objects
if (!$subject instanceof Book) {
return false;
}
return true;
}
protected function voteOnAttribute(string $attribute, $subject, TokenInterface $token) {
$user = $token->getUser();
if (!$user instanceof User) {
// the user must be logged in; if not, deny access
return false;
}
/** #var Book $book */
$book = $subject;
switch ($attribute) {
case self::VIEW:
return $this->canView($book, $user);
case self::EDIT:
return $this->canEdit($book, $user);
}
throw new \LogicException('This code should not be reached!');
}
private function canEdit(Book $book, User $user) {
// ONLY OWNER CAN EDIT BOOK
return $user === $book->getOwner();
}
private function canView(Book $book, User $user) {
// DIFFERENT LOGIC ?
return $user === $book->getOwner();
}
...
}
I'm using Symfony 3 to do a project. So I created a login page for the user. When he logs in he gets redirected to the profile page. In the profile page there is a logout button for the user to logout. What I'm trying to do is that when the user logins and logouts, the subsequent date and time should go into the database automatically.
You can save login Date and Time by listening to the security.interactive_login event. Here is how you should do it.
Create a Service for the same :
app.authentication_success_handler:
class: AppBundle\EventListener\AuthenticationSuccessHandler
arguments: ['#doctrine.orm.entity_manager', '#router']
tags:
- { name: kernel.event_listener, event: security.interactive_login, method: onSecurityInteractiveLogin }
Please note : You are passing entity_manager in arguments since you will need it to update database.
Next create a class in order to call the service :
class AuthenticationSuccessHandler implements AuthenticationSuccessHandlerInterface
{
/**
* #var type
*/
private $em;
/**
* #var type
*/
private $router;
public function __construct(EntityManager $em = null, Router $router)
{
$this->em = $em;
$this->router = $router;
}
public function onSecurityInteractiveLogin(InteractiveLoginEvent $event)
{
$token = $event->getAuthenticationToken();
$request = $event->getRequest();
$this->onAuthenticationSuccess($request, $token);
}
/**
* #param Request $request
* #param TokenInterface $token
* #return RedirectResponse
*/
public function onAuthenticationSuccess(Request $request, TokenInterface $token)
{
$path = "_security.main.target_path";
$session = $request->getSession();
$referrer = $this->router->generate('homepage'); // default redirect path.
// Handle gated content, if user has accessed before login,
if ($session->get($path)) {
$referrer = $session->get($path);
}
$user = $token->getUser();
// ############## Do database update here. Since you have access to User Object($user) and Entity manager($this->em).
$dateTime = new \DateTime();
$user->setLoginTime($dateTime);
$this->em->flush();
return new RedirectResponse($referrer);
}
}
This should work for Saving login time.
Handling Logout Time :
You can do so, by implementing a LogoutHandlerInterface and adding the handler to your security config.
Create a service :
app.logout_handler:
class: AppBundle\EventListener\LogoutListener
arguments: ['#doctrine.orm.entity_manager', '#router']
Now, make sure this service is being called when user logs out.
// security.yml
security:
firewalls:
main:
logout:
handlers: [app.logout_handler]
Now you can create another class AppBundle\EventListener\LogoutListener
which should implement LogoutHandlerInterface and save the user information like we did for app.authentication_success_handler.
I haven't tested it though. But it should work.
Hope it helps!
I'd like the user to register, confirm it's email, but being activated manually by an administrator.
Thanks to this page I found the FOSUserEvents::REGISTRATION_CONFIRMED which is called right after clicking on the confirmation link in the email.
Now I'd like to disable the account (see below).
class RegistrationListener implements EventSubscriberInterface
{
public static function getSubscribedEvents()
{
return array(
FOSUserEvents::REGISTRATION_CONFIRMED => 'onRegistrationCompleted'
);
}
public function onRegistrationCompleted(UserEvent $event) {
// registration completed
// TODO: disable the user. How?
}
}
Or is there any configuration that I missed?
Any ideas?
Thanks in advance!
As I can see, inside FOS\UserBundle\Controller\RegistrationController::
confirmAction() user is enabled:
/**
* Receive the confirmation token from user email provider, login the user.
*
* #param Request $request
* #param string $token
*
* #return Response
*/
public function confirmAction(Request $request, $token)
{
/** #var $userManager \FOS\UserBundle\Model\UserManagerInterface */
$userManager = $this->get('fos_user.user_manager');
...
$user->setConfirmationToken(null);
$user->setEnabled(true);
$event = new GetResponseUserEvent($user, $request);
$dispatcher->dispatch(FOSUserEvents::REGISTRATION_CONFIRM, $event);
$userManager->updateUser($user);
...
$dispatcher->dispatch(FOSUserEvents::REGISTRATION_CONFIRMED, new FilterUserResponseEvent($user, $request, $response));
return $response;
}
I can think of two things you can do to disable it.
1) write an event listener, that will react on FOSUserEvents::REGISTRATION_CONFIRMED and disable the user => http://symfony.com/doc/master/bundles/FOSUserBundle/controller_events.html
2) override RegistrationController => https://symfony.com/doc/current/bundles/FOSUserBundle/overriding_controllers.html
I prefer first option.
class RegistrationListener implements EventSubscriberInterface
{
/** #var EntityManager */
private $em;
/**
* #param EntityManager $em
*/
public function __construct(EntityManager $em)
{
$this->em = $em;
}
public static function getSubscribedEvents()
{
return array(
FOSUserEvents::REGISTRATION_CONFIRMED => 'onRegistrationCompleted'
);
}
public function onRegistrationCompleted(UserEvent $event) {
// registration completed
// TODO: disable the user. How?
$user = $event->getUser();
$user->setEnabled(false);
$this->em->persist($user);
$this->em->flush();
}
}
In a Symfony2.8/Doctrine2 application, I need to store in each row of my SQL tables the id of the user who created or updated the row (users can connect with Ldap).
So all my entities inherited of a GenericEntity which contains this variable (type would be string if I want to store Ldap username):
/**
* #var integer
*
* #ORM\Column(name="zzCreationId", type="string", nullable=false)
*/
private $creationId;
And I use the prePersistCallback() to automatically assign this value:
/**
* #ORM\PrePersist
*/
public function prePersistCallback()
{
$currentUser = /* ...... ????? ....... */ ;
if ($currentUser->getId() != null) {
$this->creationId = $currentUser->getId() ;
} else {
$this->creationId = 'unknown' ;
}
return $this;
}
But I don't know how to retrieve the connected user, or how to automatically inject it in the entity... How can I do it?
You can use a Doctrine entity listener/subscriber instead to inject the security token and get the current logged user:
// src/AppBundle/EventListener/EntityListener.php
namespace AppBundle\EventListener;
use Doctrine\ORM\Event\LifecycleEventArgs;
use AppBundle\Entity\GenericEntity;
use Symfony\Component\Security\Core\Authentication\Token\Storage\TokenStorageInterface;
class EntityListener
{
private $tokenStorage;
public function __construct(TokenStorageInterface $tokenStorage = null)
{
$this->tokenStorage = $tokenStorage;
}
public function prePersist(LifecycleEventArgs $args)
{
$entity = $args->getEntity();
// only act on "GenericEntity"
if (!$entity instanceof GenericEntity) {
return;
}
if (null !== $currentUser = $this->getUser()) {
$entity->setCreationId($currentUser->getId());
} else {
$entity->setCreationId(0);
}
}
public function getUser()
{
if (!$this->tokenStorage) {
throw new \LogicException('The SecurityBundle is not registered in your application.');
}
if (null === $token = $this->tokenStorage->getToken()) {
return;
}
if (!is_object($user = $token->getUser())) {
// e.g. anonymous authentication
return;
}
return $user;
}
}
Next register your listener:
# app/config/services.yml
services:
my.listener:
class: AppBundle\EventListener\EntityListener
arguments: ['#security.token_storage']
tags:
- { name: doctrine.event_listener, event: prePersist }
#ORM\PrePersist and other callback methods used in the entity are suppose to contain simple logic and be independant of other services.
You need to create event listener or subscriber to listen postPersist doctrine event and fill in corresponding attribute. Check How to Register Event Listeners and Subscribers
You can look at BlameableListener from gedmo/doctrine-extensions package, that work almost the way you want but with the username instead of the user id.
The problem
I want users to be authenticated via an access token that is supplied as a GET parameter to the first request.
I have never implemented such a thing in Symfony, so I followed the steps outlined in How to Create a custom Authentication Provider, but it 'doesn't work'. The authenticate method of the AuthenticationProviderInterface is not triggered.
What I have tried
Because it is a lot of configuration mostly, I don't even know how to debug this. This is what I have concluded so far: Only the AccessTokenProvider gets constructed, nothing else.
The code
These are the relevant parts of the system:
security.yml
security:
# Snip default (empty) in_memory provider
firewalls:
# Snip dev and main (symfony default)
accesstoken_secured:
pattern: ^/admin/
accesstoken: true
services.yml
services:
accesstoken.security.authentication.provider:
class: AppBundle\Security\Authentication\Provider\AccessTokenProvider
arguments:
- '' # User Provider
- '%kernel.cache_dir%/security/nonces'
public: false
accesstoken.security.authentication.listener:
class: AppBundle\Security\Firewall\AccessTokenListener
arguments: ['#security.token_storage', '#security.authentication.manager']
public: false
AccessTokenFactory
class AccessTokenFactory implements SecurityFactoryInterface
{
public function create(ContainerBuilder $container, $id, $config, $userProvider, $defaultEntryPoint)
{
$providerId = 'security.authentication.provider.accesstoken.'.$id;
$container
->setDefinition($providerId, new DefinitionDecorator('accesstoken.security.authentication.provider'))
->replaceArgument(0, new Reference($userProvider))
;
$listenerId = 'security.authentication.listener.accesstoken.'.$id;
$container->setDefinition($listenerId, new DefinitionDecorator('accesstoken.security.authentication.listener'));
return array($providerId, $listenerId, $defaultEntryPoint);
}
public function getPosition()
{
return 'pre_auth';
}
public function getKey()
{
return 'accesstoken';
}
public function addConfiguration(NodeDefinition $node)
{
}
}
AccessTokenProvider
class AccessTokenProvider implements AuthenticationProviderInterface
{
private $userProvider;
public function __construct(UserProviderInterface $userProvider)
{
$this->userProvider = $userProvider;
}
public function authenticate(TokenInterface $token)
{
$user = $this->userProvider->loadUserByAccessToken($token->getAttribute('token'));
if ($this->isTokenValid($token)) {
$authenticatedToken = new AccessToken(['role_user']);
$authenticatedToken->setUser($user);
return $authenticatedToken;
}
throw new AuthenticationException('The WSSE authentication failed.');
}
protected function isTokenValid(AccessToken $token)
{
//TODO: Implement
return (bool)$token->token;
}
public function supports(TokenInterface $token)
{
return $token instanceof AccessToken;
}
}
AccessTokenListener
class AccessTokenListener
{
protected $tokenStorage;
protected $authenticationManager;
/**
* AccessTokenListener constructor.
* #param TokenStorageInterface $tokenStorage
* #param AuthenticationManagerInterface $authenticationManager
*/
public function __construct(TokenStorageInterface $tokenStorage, AuthenticationManagerInterface $authenticationManager)
{
$this->tokenStorage = $tokenStorage;
$this->authenticationManager = $authenticationManager;
}
public function handle(GetResponseEvent $event)
{
$request = $event->getRequest();
$accesstoken = $request->get('accesstoken');
$token = new AccessToken();
$token->token = $accesstoken;
try {
$authToken = $this->authenticationManager->authenticate($token);
$this->tokenStorage->setToken($authToken);
return;
} catch (AuthenticationException $failed) {
// ... you might log something here
}
// By default deny authorization
$response = new Response();
$response->setStatusCode(Response::HTTP_FORBIDDEN);
$event->setResponse($response);
}
}
AccessToken
class AccessToken extends AbstractToken
{
public $token;
/**
* AccessToken constructor.
* #param array $roles
*/
public function __construct(array $roles = array())
{
parent::__construct($roles);
// If the user has roles, consider it authenticated
$this->setAuthenticated(count($roles) > 0);
}
/**
* Returns the user credentials.
*
* #return mixed The user credentials
*/
public function getCredentials()
{
return '';
}
}
I eventually tried to implement it in another way, using the tutorial at How to Create a Custom Authentication System with Guard.
This uses Symfony's new Guard system. It was actually very easy to setup!