Symfony 2.7 - Unable to use render() - onAuthenticationFailure() - php

I have a file "AuthenticationHandler.php" which is using "onAuthenticationFailure()" function to be able to lock an account after 5 failed attempts. If an account is locked, an email is sent to this user with an unlock link to his account... Everything is working fine here.
The problem is when I try to use "render()" instead of simply adding a sentence in "setBody("something", 'text/html')"
I have tried injecting #twig/#templating/#service_container in my AuthenticationHandler but none of them worked. When I fail on purpose to log in, I'm not receiving any error nor any flashbag saying "Bad Credential". It simply does nothing. It goes back to the login page without any message or mail.
I know my imports(use) are good because if I ommit them, I receive some common error where it says that I forgot a use statement.
I have tried putting a try/catch around "render()", but it simply ignores the rest of my code the moment I reach "$this->template->render('email:myTestPage.html.twig')
Again, every other service I inject works fine, If I simply don't use the "render()" function, an email is sent correctly. Is there any other way to capture an error message? Or at least something to put me on the right track!
service.yml:
authentication_handler:
class: Dbm\UserBundle\Handler\AuthenticationHandler
arguments: ["#session", "#router", "#security.context", "#doctrine.orm.entity_manager", "#mailer", "#twig"]
AuthenticationHandler.php
use Symfony\Component\Security\Http\Authentication\AuthenticationFailureHandlerInterface;
use Symfony\Component\Security\Http\Authentication\AuthenticationSuccessHandlerInterface;
use Symfony\Component\Security\Core\Exception\AuthenticationException;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\RedirectResponse;
use Symfony\Component\Security\Core\Authentication\Token\TokenInterface;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\HttpFoundation\Session\Session;
use Symfony\Component\Security\Core\Exception\BadCredentialsException;
use Symfony\Component\Security\Core\SecurityContext;
use Symfony\Component\Routing\Router;
use Doctrine\ORM\EntityManager;
use Symfony\Bundle\FrameworkBundle\Templating\EngineInterface;
class AuthenticationHandler implements AuthenticationSuccessHandlerInterface, AuthenticationFailureHandlerInterface
{
private $session;
private $security;
private $router;
private $em;
private $mailer;
private $twig;
public function __construct(Session $session, Router $router, SecurityContext $security, EntityManager $em, \Swift_Mailer $mailer, \Twig_Environment $twig)
{
$this->session = $session;
$this->security = $security;
$this->router = $router;
$this->em = $em;
$this->mailer = $mailer;
$this->twig = $twig;
}
public function onAuthenticationFailure(Request $request, AuthenticationException $exception)
{
...
$message = (new \Swift_Message())
->setSubject('Unlock Account')
->setFrom('foobar#foobar.com')
->setTo($mail)
->setBody($this->twig->render('Email:new-unexpire-account.html.twig', array('hash' => $hash)), 'text/html');
$this->mailer->send($message);
}
}
I have also tried with $foo = $this->twig->render('Email:new-unexpire-account.html.twig', array('hash' => $hash)), 'text/html') alone and I'm not getting anything either. It stops there.

Related

How to retrieve azure-user's group memberships?

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()]);

Why the sendMailConfirmation method does not send a signedURL to the user?

By using Symfony 5 and after user registration, the system should send an automatic email to the user to activate a signed URL. In EmailVerifer.php, the sendMailConfirmation method does not recognize the getId and getEmail methods. However after registration the confirmation email is anyway sent by the system to the user but without a signed URL to activate it. I think the issue is related to the implementation of serialization and deserialization methods in user class, as it worked before Serialization implementation. I'm trying to implement serializable methods in EmailVerifier class but it doesn't work. Thanks for the help.
<?php
namespace App\Security;
use Doctrine\ORM\EntityManagerInterface;
use Symfony\Bridge\Twig\Mime\TemplatedEmail;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\Mailer\MailerInterface;
use Symfony\Component\Security\Core\User\UserInterface;
use SymfonyCasts\Bundle\VerifyEmail\Exception\VerifyEmailExceptionInterface;
use SymfonyCasts\Bundle\VerifyEmail\VerifyEmailHelperInterface;
class EmailVerifier
{
private $verifyEmailHelper;
private $mailer;
private $entityManager;
public function __construct(VerifyEmailHelperInterface $helper, MailerInterface $mailer, EntityManagerInterface $manager)
{
$this->verifyEmailHelper = $helper;
$this->mailer = $mailer;
$this->entityManager = $manager;
}
public function sendEmailConfirmation(string $verifyEmailRouteName, UserInterface $user, TemplatedEmail $email): void
{
$signatureComponents = $this->verifyEmailHelper->generateSignature(
$verifyEmailRouteName,
$user->getId(),
$user->getEmail()
);
$context = $email->getContext();
$context['signedUrl'] = $signatureComponents->getSignedUrl();
$context['expiresAtMessageKey'] = $signatureComponents->getExpirationMessageKey();
$context['expiresAtMessageData'] = $signatureComponents->getExpirationMessageData();
$email->context($context);
$this->mailer->send($email);
}
/**
* #throws VerifyEmailExceptionInterface
*/
public function handleEmailConfirmation(Request $request, UserInterface $user): void
{
$this->verifyEmailHelper->validateEmailConfirmation($request->getUri(), $user->getId(), $user->getEmail());
$user->setIsVerified(true);
$this->entityManager->persist($user);
$this->entityManager->flush();
}
}

Redirect from event subscriber

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

Symfony - using twig in onKernelRequest kills session

I have a strange issue in symfony 4.3 (also tested it in 4.2 - same behaviour) - I am using an EventListener to process a request - heres the code:
<?php
namespace App\EventListener;
use App\Entity\Company;
use Doctrine\ORM\EntityManagerInterface;
use Symfony\Component\EventDispatcher\EventSubscriberInterface;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\HttpKernel\Event\RequestEvent;
use Symfony\Component\HttpKernel\KernelEvents;
use Twig\Environment;
class ShopListener implements EventSubscriberInterface
{
/** #var EntityManagerInterface */
protected $em;
/** #var Environment */
protected $twig;
public function __construct(EntityManagerInterface $entityManager, Environment $twig)
{
$this->em=$entityManager;
$this->twig=$twig;
}
public function onKernelRequest(RequestEvent $event)
{
if($event->isMasterRequest()===false) {
return;
}
/** #var Request $request */
$request=$event->getRequest();
$subDomain=$request->attributes->get('domain');
if($subDomain===null) {
return;
}
$company=$this->em->getRepository(Company::class)->findOneBy([
'subDomain' => $subDomain,
]);
if($company instanceof Company && $company->shopIsOnline()) {
$request->attributes->set('company',$company);
return;
}
$event->setResponse(
new Response($this->twig->render('page/shop_not_found.html.twig'),404)
);
}
public static function getSubscribedEvents(): array
{
return [
KernelEvents::REQUEST => ['onKernelRequest',0],
];
}
}
After registering that listener, $request->getSession() is always null in my controller (toolbar also notices, that there is no session registered). When deregistering it, the session is there, but the logic in the listener is skipped. I have tried to play around with the priority to ensure, there's no other listener which interferes.
It seems, that already registering that event kills the session (even if onKernelRequest is empty), which is hard to believe.
What am I missing?
Session is created by Symfony\Component\FrameworkBundle\EventListener\SessionListener listener, on kernel.request event too (priority of 128).
This event has a specific behavior: if a listener sets a Response, "the process skips directly to the kernel.response event" to quote the documentation. I would suspect it could causes issues.
Try setting your listener a priority < 0 (I'm getting you tried many), and please checks the order the profiler "Events" section (/_profiler/latest?panel=events).
Found the solution - problem is the injection of the twig-environment in the constructor - without twig everything works as expected. I guess, loading the twig-environment at this stage does something to the session (like loading it too early).
I moved the listener to onKernelController and modified it:
<?php
namespace App\EventListener;
use App\Entity\Company;
use Doctrine\ORM\EntityManagerInterface;
use Symfony\Component\EventDispatcher\EventSubscriberInterface;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\HttpKernel\Event\ControllerEvent;
use Symfony\Component\HttpKernel\KernelEvents;
use Twig\Environment;
class ShopListener implements EventSubscriberInterface
{
/** #var EntityManagerInterface */
protected $em;
/** #var Environment */
protected $twig;
public function __construct(EntityManagerInterface $entityManager, Environment $twig)
{
$this->em=$entityManager;
$this->twig=$twig;
}
public function onKernelController(ControllerEvent $controllerEvent)
{
if($controllerEvent->isMasterRequest()===false) {
return;
}
/** #var Request $request */
$request=$controllerEvent->getRequest();
$subDomain = $request->attributes->get('domain');
if($subDomain===null) {
return;
}
$company=$this->em->getRepository(Company::class)->findOneBy([
'subDomain' => $subDomain,
]);
if($company instanceof Company && $company->shopIsOnline()) {
$request->attributes->set('company',$company);
return;
}
$controllerEvent->setController(
function() {
return new Response($this->twig->render('page/shop_not_found.html.twig'),404);
}
);
}
public static function getSubscribedEvents(): array
{
return [
KernelEvents::CONTROLLER => ['onKernelController',128],
];
}
}

Symfony logout after inactivity, make log entry

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);
}
}

Categories