based on this question
I have implemented an automatic logout of users after a certain period of inactivity (like in question above). This works fine, but I need to make a log entry for this event.
The problem is that when logout fires, I get multiple records in my log file instead of 1 record. I guess I need to listen to some other request, instead of onKernelRequest. Any ideas how to do that? My code is as follows:
<?php
namespace AppBundle\EventListener;
use Symfony\Component\HttpKernel\HttpKernelInterface;
use Symfony\Component\HttpKernel\Event\GetResponseEvent;
use Symfony\Component\HttpFoundation\Session\SessionInterface;
use Symfony\Component\HttpFoundation\Session\Session;
use Symfony\Component\Routing\RouterInterface;
use Symfony\Component\HttpFoundation\RedirectResponse;
use Symfony\Component\Security\Core\Authentication\Token\Storage\TokenStorage;
use Symfony\Component\Security\Core\Exception\CredentialsExpiredException;
class RequestListener{
protected $session;
protected $securityToken;
protected $router;
protected $logger;
protected $maxIdleTime;
public function __construct(Session $session, TokenStorage $securityToken, RouterInterface $router, $logger, $maxIdleTime)
{
$this->session = $session;
$this->securityToken = $securityToken;
$this->router = $router;
$this->logger = $logger;
$this->maxIdleTime = $maxIdleTime;
}
public function onKernelRequest(GetResponseEvent $event)
{
if (HttpKernelInterface::MASTER_REQUEST != $event->getRequestType()) {
return;
}
if ($this->maxIdleTime > 0) {
$lapse = time() - $this->session->getMetadataBag()->getCreated();
if ($lapse > $this->maxIdleTime) {
$username = $this->securityToken->getToken()->getUser();
if ($username !== 'anon.'){
$username = $username->getUsername();
}
$this->securityToken->setToken(null);
$this->session->getFlashBag()->set('error', 'Your session expired, you need to login again');
$this->session->invalidate();
$this->logger->makelog(//I get multiple log entries here instead of 1
0,
'Session timeout',
$username
);
$event->setResponse(new RedirectResponse($this->router->generate('login')));
}
}
}
}
UPD_1
I have already created a logout listener, but it listens only for logout event when the Logout button is pressed and this action is logged with different log entry. In my code above I use $this->session->invalidate() in order to logout the user. My code for logout listener is as follows:
<?php
namespace AppBundle\EventListener;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\Security\Core\Authentication\Token\TokenInterface;
use Symfony\Component\Security\Core\Authentication\Token\Storage\TokenStorage;
use Symfony\Component\Security\Http\Logout\LogoutHandlerInterface;
use Doctrine\ORM\EntityManager;
class LogoutListener implements LogoutHandlerInterface
{
protected $securityContext;
protected $entityManager;
protected $logger;
public function __construct(TokenStorage $securityContext, EntityManager $entityManager, $logger)
{
$this->securityContext = $securityContext;
$this->entityManager = $entityManager;
$this->logger = $logger;
}
public function logout(Request $Request, Response $Response, TokenInterface $Token)
{
$em = $this->entityManager;
$user = $this->securityContext->getToken()->getUser();
$this->logger->makelog(1, 'Logout action, logout button', $user);
}
}
Related
I'm using knpuniversity's oauthbundle in symfony 5.4 and I have a fairly straightforward implementation of the AzureAuthenticator.
When I fetch the user from Microsoft365 I would like to also retrieve the group memberships of that user. It seems this requires a separate API call, or is there a smoother way to do this?
Can I control in the azure portal what gets passed in the callback? Currently it only holds email, name and a few other values, nothing related to groups.
<?php
namespace App\Security;
use App\Entity\WebtoolsUser as User;
use Doctrine\ORM\EntityManagerInterface;
use KnpU\OAuth2ClientBundle\Client\ClientRegistry;
use KnpU\OAuth2ClientBundle\Security\Authenticator\OAuth2Authenticator;
use KnpU\OAuth2ClientBundle\Client\Provider\AzureClient;
use Symfony\Component\HttpFoundation\RedirectResponse;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\Routing\RouterInterface;
use Symfony\Component\Security\Core\Authentication\Token\TokenInterface;
use Symfony\Component\Security\Core\Exception\AuthenticationException;
use Symfony\Component\Security\Http\Authenticator\Passport\Badge\UserBadge;
use Symfony\Component\Security\Http\Authenticator\Passport\PassportInterface;
use Symfony\Component\Security\Http\Authenticator\Passport\SelfValidatingPassport;
class AzureAuthenticator extends OAuth2Authenticator
{
private $clientRegistry;
private $em;
private $router;
public function __construct(ClientRegistry $clientRegistry, EntityManagerInterface $em, RouterInterface $router)
{
$this->clientRegistry = $clientRegistry;
$this->em = $em;
$this->router = $router;
}
public function supports(Request $request): ?bool
{
// continue ONLY if the current ROUTE matches the check ROUTE
return $request->attributes->get('_route') === 'login_microsoft_check';
}
public function authenticate(Request $request): PassportInterface
{
$client = $this->clientRegistry->getClient('azure');
$accessToken = $this->fetchAccessToken($client);
return new SelfValidatingPassport(
new UserBadge($accessToken->getToken(), function() use ($accessToken, $client) {
$azureUser = $client->fetchUserFromToken($accessToken);
IS SEPARATE CALL NEEDED HERE? ------->
$email = $azureUser->claim('unique_name');
// 1) have they logged in with a Microsoft account ? Easy!
$existingUser = $this->em->getRepository(User::class)->findOneBy(['azureId' => $azureUser->getId()]);
I want to redirect user conditionnaly from my event subscriber on kernel.controller event and change it if user can't acces the one asked.
<?php
namespace App\EventSubscriber;
use Symfony\Component\EventDispatcher\EventSubscriberInterface;
use Symfony\Component\HttpKernel\Event\ControllerEvent;
use Symfony\Component\HttpKernel\KernelEvents;
use Symfony\Component\HttpFoundation\Session\SessionInterface;
use Symfony\Component\DependencyInjection\ParameterBag\ParameterBagInterface;
use Symfony\Component\HttpFoundation\RedirectResponse;
use Symfony\Component\Routing\RouterInterface;
class WebPageAccessSuscriber implements EventSubscriberInterface
{
/**
* #var SessionInterface
*/
private $session;
/**
* Contient l'ensemble des parametres de config
*
* #var ParameterBagInterface
*/
private $params;
private $router;
public function __construct(SessionInterface $session, ParameterBagInterface $params, RouterInterface $router)
{
// Retrieve the client session
$this->session = $session;
// Retrieve configuration variable
$this->params = $params;
$this->router = $router;
}
public function onKernelController(ControllerEvent $event)
{
error_log(__METHOD__);
$controller = $event->getController();
if(!is_array($controller)) return;
if ($this->params->get('visitor_access') === false && $controller[0] instanceof \App\Controller\restictedController) {
$event->setController(function() use ($event) {
return new RedirectResponse($this->router->generate('login_' . $event->getRequest()->getLocale()));
});
}
}
public static function getSubscribedEvents()
{
return [
// must be registered before (i.e. with a higher priority than) the default Locale listener
KernelEvents::CONTROLLER => [['onKernelController']]
];
}
}
It works but symfony web profiler doesn't shows anymore.
I mean it does but his content is the one from my login template
Why symfony is redirecting webprofiler to login page ? How can prevent this behavior ? Should I use another way to achieve this ?
If you want to protect a whole section of your application use the symfony security component to create a firewall with its own authentication.
If you want to protect specific pages you could use a Security Voter https://symfony.com/doc/current/security/voters.html
Edit: After denying access via a Voter you can redirect the user with a https://symfony.com/doc/current/security/access_denied_handler.html
Alright so what am I trying to do is that I check if user status is "pending" and if so, I'd redirect him to "/pending" page.
Now I need this check on almost the entire website.
I tried with the decision manager but was unable to redirect, any other way to do this?
This should be called only for logged users
security.yaml
access_decision_manager:
service: App\Security\StatusAuthenticator
And the StatusAuthenticator
<?php
namespace App\Security;
use App\Entity\User;
use Symfony\Component\Security\Core\Authentication\Token\TokenInterface;
use Symfony\Component\Security\Core\Authorization\AccessDecisionManagerInterface;
class StatusAuthenticator implements AccessDecisionManagerInterface
{
/**
* #param TokenInterface $token
* #param array $attributes
* #param null $object
* #return bool|void
*/
public function decide(TokenInterface $token, array $attributes, $object = null)
{
if($token->getUser()->getStatus() == User::USER_STATUS_PENDING) {
// Needs to be redirected to /pending
return false;
}
return true;
}
}
Since you need to "check this on almost the entire website", you can use an EventListener that will fire on every request and there you can check if you have an authenticated user and their status.
// src/EventListener/PendingUserListener.php
namespace App\EventListener;
use App\Entity\User;
use Symfony\Component\EventDispatcher\EventSubscriberInterface;
use Symfony\Component\HttpFoundation\RedirectResponse;
use Symfony\Component\HttpKernel\Event\RequestEvent;
use Symfony\Component\HttpKernel\KernelEvents;
use Symfony\Component\Routing\Generator\UrlGeneratorInterface;
use Symfony\Component\Security\Core\Security;
use Symfony\Component\Security\Core\User\UserInterface;
class PendingUserListener implements EventSubscriberInterface
{
/**
* #var Security
*/
private $security;
/**
* #var UrlGeneratorInterface
*/
private $urlGenerator;
public function __construct(Security $security, UrlGeneratorInterface $urlGenerator)
{
$this->security = $security;
$this->urlGenerator = $urlGenerator;
}
public static function getSubscribedEvents()
{
return [ KernelEvents::REQUEST => 'onKernelRequest' ];
}
public function onKernelRequest(RequestEvent $event)
{
$pending_route = 'pending';
$user = $this->security->getUser();
if (!$event->isMasterRequest()) {
return;
}
if (!$user instanceof UserInterface) {
return;
}
// Check if the requested page is 'pending', prevent redirect loops
if ($pending_route === $event->getRequest()->get('_route')) {
return;
}
// RedirectResponse expects a full url, generate from route name
if (User::USER_STATUS_PENDING == $user->getStatus()) {
$event->setResponse(
new RedirectResponse($this->urlGenerator->generate($pending_route))
);
}
}
}
My Question
What sort of Response should I return that won't change the default response? Or is there a better way to tack on a logger to a Login Failure/badcredentialsexception?
Details
I found this post here which states that you can (in Symfony 2.4) customize authentication failures or successes like so:
use Symfony\Component\Security\Core\Exception\AuthenticationException;
use Symfony\Component\Security\Http\Authentication\AuthenticationFailureHandlerInterface;
use Symfony\Component\Security\Http\Authentication\AuthenticationSuccessHandlerInterface;
use Symfony\Component\Security\Core\Authentication\Token\TokenInterface;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
class CustomTimeAuthenticator extends TimeAuthenticator implements AuthenticationFailureHandlerInterface, AuthenticationSuccessHandlerInterface
{
public function onAuthenticationFailure(Request $request, AuthenticationException $exception)
{
error_log('You are out!');
}
public function onAuthenticationSuccess(Request $request, TokenInterface $token)
{
error_log(sprintf('Yep, you are in "%s"!', $token->getUsername()));
}
}
It also states that
...you can also bypass the default behavior altogether by returning a
Response instance:
public function onAuthenticationFailure(Request $request, AuthenticationException $exception)
{
if ($exception->getCode()) {
return new Response('Not the right time to log in, come back later.');
}
}
Unfortunately it seems in Symfony 4 you have to return a Response (unlike the above 2.4 code) and so my code is:
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\Security\Http\Authentication\AuthenticationFailureHandlerInterface;
use Symfony\Component\Security\Core\Exception\AuthenticationException;
use Symfony\Component\Security\Core\Authentication\Token\Storage\TokenStorageInterface;
use Psr\Log\LoggerInterface;
class LoginFailureLogger implements AuthenticationFailureHandlerInterface
{
private $logger;
private $security;
public function __construct(TokenStorageInterface $security, LoggerInterface $logger)
{
$this->logger = $logger;
$this->security = $security;
}
public function onAuthenticationFailure(Request $request, AuthenticationException $exception)
{
$user = $exception->getToken()->getUser();
$this->logger->notice('Failed to login user: "'. $user. '"". Reason: '. $exception->getMessage());
}
}
But when the page runs I get:
Authentication Failure Handler did not return a Response.
You should just redirect to login page since this is the default behaviour. Please modify upon your specific requirements if any.
use Symfony\Component\HttpFoundation\RedirectResponse;
use Symfony\Component\HttpFoundation\Session\Flash\FlashBagInterface;
...
private $flashBag;
private $logger;
private $security;
public function __construct(TokenStorageInterface $security, LoggerInterface $logger, FlashBagInterface $flashBag)
{
$this->logger = $logger;
$this->security = $security;
$this->flashBag = $flashBag;
}
public function onAuthenticationFailure(Request $request, AuthenticationException $exception)
{
$user = $exception->getToken()->getUser();
$this->logger->notice('Failed to login user: "'. $user. '"". Reason: '. $exception->getMessage());
$this->flashBag()->add('notice', 'Failed to login.');
return new RedirectResponse('/login');
}
EDIT: Added flash message
I got my custom logout handler which is the following:
namespace AppBundle\EventListener;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\Security\Core\Authentication\Token\TokenInterface;
use Symfony\Component\Security\Core\Authentication\Token\Storage\TokenStorage;
use Symfony\Component\Security\Http\Logout\LogoutHandlerInterface;
use FOS\UserBundle\Model\UserManagerInterface;
use Doctrine\ORM\EntityManager;
use AppBundle\Entity\User;
class LogoutListener implements LogoutHandlerInterface
{
protected $securityContext;
protected $entityManager;
protected $logger;
public function __construct(TokenStorage $securityContext, EntityManager $entityManager, $logger)
{
$this->securityContext = $securityContext;
$this->entityManager = $entityManager;
$this->logger = $logger;
}
public function logout(Request $Request, Response $Response, TokenInterface $Token)
{
$em = $this->entityManager;
$user = $this->securityContext->getToken()->getUser();
$em->getConnection()->executeUpdate("UPDATE subjects SET edited_by = NULL
WHERE edited_by=" . $user->getId());
$this->logger->makelog(1, 'Выход из системы');
}
Here I log into a file all logout actions. Now I need to differentiate between user logout upon button hit, and session expiration. Any ideas how to do that? What service should I implement. Now logout upon session expiration is simply handled in config.yml
session:
save_path: "%kernel.root_dir%/../var/sessions/%kernel.environment%"
cookie_lifetime: 3600
gc_maxlifetime: 3600