Unsupported methods in Symfony - php

I am implementing in my symfony project 2Fa authentication with the help of the official documentation. All good until I try to customize the conditions so that the user is forced to go through the 2Fa form, using the documentation method.
The problem starts when in the function shouldPerformTwoFactorAuthentication I start to implement code as if it were a common function, I try do this:
public function shouldPerformTwoFactorAuthentication(AuthenticationContextInterface $context, ManagerRegistry $doctrine, UserInterface $userInterface): bool
{
$entityManager = $doctrine->getManager();
$user = $entityManager->getRepository(User::class)->findOneBy(['email' => $userInterface->getUserIdentifier()]);
if ($user->isIsGoogleEnabled()) {
return false;
} else {
return false;
}
}
As you can see I try to customize when to make it work and when not, but I get this error:
Method 'App\Controller\TwoFactorConditionController::shouldPerformTwoFactorAuthentication()' is not compatible with method 'Scheb\TwoFactorBundle\Security\TwoFactor\Condition\TwoFactorConditionInterface::shouldPerformTwoFactorAuthentication()'.intelephense(1038)
I've been searching the internet with no results that work for me, until i found this.
I have made so many attempts at this that I will not post them, if someone can guide me, I appreciate your time, thanks!

This will work using __construct() method
private EntityManagerInterface $entityManager;
private UserInterface $userInterface;
public function __construct(EntityManagerInterface $entityManager, UserInterface $userInterface)
{
$this->entityManager = $entityManager;
$this->userInterface = $userInterface;
}
public function shouldPerformTwoFactorAuthentication(AuthenticationContextInterface $context): bool
{
$user = $this->entityManager->getRepository(User::class)->findOneBy(['email' => $this->userInterface->getUserIdentifier()]);
if ($user->isIsGoogleEnabled()) {
return false;
} else {
return false;
}
}

Related

Send responseon EventSubscriber Symfony 3.4

I'm trying to set a response on an eventsubscriber that checks if an API authorization token it's correct
class TokenSubscriber implements EventSubscriberInterface
{
private $em;
public function __construct(EntityManager $em)
{
$this->em = $em;
}
public function onKernelController(FilterControllerEvent $event)
{
$controller = $event->getController();
if ($controller[0] instanceof TokenAuthenticatedController) {
$apiKey = $this->em->getRepository('AppBundle:ApiKey')->findOneBy(['enabled' => true, 'name' => 'apikey'])->getApiKey();
$token = $event->getRequest()->headers->get('x-auth-token');
if ($token !== $apiKey) {
//send response
}
}
}
public static function getSubscribedEvents()
{
return [
KernelEvents::CONTROLLER => 'onKernelController',
];
}
}
But I cant stop the current request and return a respone as a controller, what is the correct way to send a response with an error message and stop the current request
You can not do that using the FilterControllerEvent Event. On that moment, symfony already decided which controller to execute. I think you might want to look into the Symfony Security component. It can protect routes like what you want, but in a slightly different way (access_control and/or annotations).
If you want to block access to an API (eg. JSON), you easily follow this doc. You can also mix it using the Security annotations on your controllers or actions using this doc
I think you can throw an error here
throw new AccessDeniedHttpException('Your message here!');

Symfony -Return json output of Command Class from Controller

In my Symfony project I have created a Command class to delete specific user.
I injected required parameter "email" in the Command class constructor.
I have never tried to implement command in the Controller so I have problem there.
I want to trigger the API call in the Controller which will return desired json output if command is successful.
How can I accomplish that?
My Command class:
protected static $defaultName = 'user:delete';
$entityManager;
private $userService;
private $email;
public function __construct(string $email = null, EntityManagerInterface $entityManager, KeycloakApi $keycloakApi)
{
parent::__construct($email);
$this->entityManager = $entityManager;
$this->userService = $userService;
}
protected function configure()
{
$this
->setDescription('Deletion of selected user.')
->addArgument('email', InputArgument::REQUIRED, 'User email');
}
protected function execute(InputInterface $input, OutputInterface $output)
{
$user = $this->userService->getUserByEmail($this->email);
if (empty($user)) {
throw new Exception('USER_DOESNT_EXIST');
}
$this->userService->deleteUser($user['id']);
$output->writeln('Done!');
}
And my try in controller to get what I want:
/**
* #Route("/delete/test", name="delete_test")
*/
public function testDelete(): JsonResponse
{
$application = new Application($this->kernel);
$application->setAutoExit(false);
$input = new ArrayInput(array("user:delete"));
$output = new BufferedOutput();
// Run the command
$retval = $application->run($input, $output);
dump($retval);die;
}
And the main question is how to pass email parameter in command that is needed to be provided for this endpoint?
As the comments said, you shouldn't call a command from a controller, both are a different entry point of your application. Controller are used to access from the web and Command to be executed from a cli.
You should put your domain logic in neither of those files.
Here's an example which is possible :
Controller :
/**
* #Route("/delete/{email}", name="delete")
*/
public function delete(string $email, DeleteUserHandler $handler): JsonResponse
{
$handler->handle($email);
return new JsonReponse(null, 204);
}
DeleteUserHandler :
...
public function handle(string $email): void
{
$user = $this->userService->getUserByEmail($this->email);
if (empty($user)) {
throw new Exception('USER_DOESNT_EXIST');
}
$this->userService->deleteUser($user['id']);
}
I kept a bit of you code but IMO you don't even need to find before the delete (just maybe add a security to avoid anyone to delete anyone)
With this kind of code you can reuse the "DeleteUserHandler" in a command (or wherever) if you need it and the code which really delete a user isn't coupled with the entry point anymore

How to provide Symfony routing parameter programatically?

In this Symfony route
/**
* #Route("/board/{board}/card/{card}", name="card_show", methods={"GET"}, options={})
*/
public function show(Board $board, Card $card): Response
{
$card->getLane()->getBoard(); // Board instance
// ...
}
How is it possible to add the {board} parameter programatically, since it is already available in {card}? Now, I always need to add two parameters, when generating links to show action.
After some research I've found the RoutingAutoBundle (https://symfony.com/doc/master/cmf/bundles/routing_auto/introduction.html#usage) which would provide the functions I need, but it's not available for Symfony 5 anymore.
Thanks.
Okay, after some investigation I've found this question
Which lead me to this helpful answer.
My controller action (with #Route annotation) looks like this:
/**
* #Route("/board/{board}/card/{card}", name="card_show", methods={"GET"})
*/
public function show(Card $card): Response
{
}
We just have one argument ($card) in method signature, but two arguments in route.
This is how to call the route in twig:
path("card_show", {card: card.id})
No board parameter required, thanks to a custom router.
This is how the custom router looks like:
<?php // src/Routing/CustomCardRouter.php
namespace App\Routing;
use App\Repository\CardRepository;
use Symfony\Component\Routing\RouterInterface;
class CustomCardRouter implements RouterInterface
{
private $router;
private $cardRepository;
public function __construct(RouterInterface $router, CardRepository $cardRepository)
{
$this->router = $router;
$this->cardRepository = $cardRepository;
}
public function generate($name, $parameters = [], $referenceType = self::ABSOLUTE_PATH)
{
if ($name === 'card_show') {
$card = $this->cardRepository->findOneBy(['id' => $parameters['card']]);
if ($card) {
$parameters['board'] = $card->getLane()->getBoard()->getId();
}
}
return $this->router->generate($name, $parameters, $referenceType);
}
public function setContext(\Symfony\Component\Routing\RequestContext $context)
{
$this->router->setContext($context);
}
public function getContext()
{
return $this->router->getContext();
}
public function getRouteCollection()
{
return $this->router->getRouteCollection();
}
public function match($pathinfo)
{
return $this->router->match($pathinfo);
}
}
Now, the missing parameter board is provided programatically, by injecting and using the card repository. To enable the custom router, you need to register it in your services.yaml:
App\Routing\CustomCardRouter:
decorates: 'router'
arguments: ['#App\Routing\CustomCardRouter.inner']

Check user in every controller in Symfony

I have a user object that has a property 'enabled'. I want every action to first check if the user is enabled before continuing.
Right now I have solved it with a Controller that every other controller extends, but using the setContainer function to catch every Controller action feels really hacky.
class BaseController extends Controller{
public function setContainer(ContainerInterface $container = null)
{
$this->container = $container;
$user = $this->getUser();
// Redirect disabled users to a info page
if (!$user->isEnabled() && !$this instanceof InfoController) {
return $this->redirectToRoute('path_to_info');
}
}
I have tried building this using a before filter (http://symfony.com/doc/current/event_dispatcher/before_after_filters.html), but could not get the User object..any tips?
EDIT:
This is my solution:
namespace AppBundle\Security;
use AppBundle\Controller\AccessDeniedController;
use AppBundle\Controller\ConfirmController;
use Symfony\Bundle\FrameworkBundle\Routing\Router;
use Symfony\Bundle\TwigBundle\Controller\ExceptionController;
use Symfony\Component\HttpFoundation\RedirectResponse;
use Symfony\Component\HttpKernel\Event\FilterControllerEvent;
use Symfony\Component\Security\Core\Authentication\Token\Storage\TokenStorage;
class UserEnabledListener
{
private $tokenStorage;
private $router;
public function __construct(TokenStorage $tokenStorage, Router $router)
{
$this->tokenStorage = $tokenStorage;
$this->router = $router;
}
public function onKernelController(FilterControllerEvent $event)
{
$controller = $event->getController();
/*
* $controller passed can be either a class or a Closure.
* This is not usual in Symfony but it may happen.
* If it is a class, it comes in array format
*/
if (!is_array($controller)) {
return;
}
$controller = $controller[0];
// Skip enabled check when:
// - we are already are the AccessDenied controller, or
// - user confirms e-mail and becomes enabled again, or
// - Twig throws error in template
if ($controller instanceof AccessDeniedController ||
$controller instanceof ConfirmController ||
$controller instanceof ExceptionController) {
return;
}
$user = $this->tokenStorage->getToken()->getUser();
// Show info page when user is disabled
if (!$user->isEnabled()) {
$redirectUrl = $this->router->generate('warning');
$event->setController(function() use ($redirectUrl) {
return new RedirectResponse($redirectUrl);
});
}
}
}
EDIT 2:
Ok so turns out checking for each controller manually is really bad, as you will miss Controllers from third party dependencies. I'm going to use the Security annotation and do further custom logic in a custom Exception controller or template etc.
You can use an event listener to listen for any new request.
You'll need to inject the user and then do your verification:
<service id="my_request_listener" class="Namespace\MyListener">
<tag name="kernel.event_listener" event="kernel.request" method="onKernelRequest" />
<argument type="service" id="security.token_storage" />
</service>
Edit: Here is a snippet to give an example
class MyRequestListener {
private $tokenStorage;
public function __construct(TokenStorage $tokenStorage)
{
$this->tokenStorage = $tokenStorage;
}
public function onKernelRequest(GetResponseEvent $event)
{
if (!$event->getRequest()->isMasterRequest()) {
// don't do anything if it's not the master request
return;
}
if ($this->tokenStorage->getToken()) {
$user = $this->tokenStorage->getToken()->getUser();
//do your verification here
}
}
In your case I would use the #Security annotation, which can be very flexible if you use the expression language.
use Sensio\Bundle\FrameworkExtraBundle\Configuration\Security;
/**
* #Security("user.isEnabled()")
*/
class EventController extends Controller
{
// ...
}
In the end it's only 1 line in each of your controller files, and it has the advantage of being very readable (a developer new to the project would know immediately what is going on without having to go and check the contents of a BaseController or any potential before filter...)
More documentation on this here.
You can override also getuser() function in your BaseController also.
/**
* Get a user from the Security Token Storage.
*
* #return mixed
*
* #throws \LogicException If SecurityBundle is not available
*
* #see TokenInterface::getUser()
*/
protected function getUser()
{
if (!$this->container->has('security.token_storage')) {
throw new \LogicException('The SecurityBundle is not registered in your application.');
}
if (null === $token = $this->container->get('security.token_storage')->getToken()) {
return;
}
if (!is_object($user = $token->getUser())) {
// e.g. anonymous authentication
return;
}
// Redirect disabled users to a info page
if (!$user->isEnabled() && !$this instanceof InfoController) {
return $this->redirectToRoute('path_to_info');
}
return $user;
}

Anonymous user object in symfony

I'm using the basic user login/logout system provided with Symfony and it works fine as long as people log in. In that case the $user object is always provided as needed.
The problem is then when logged out (or not lgged in yet) there is no user object. Is there a possibility to have (in that case) a default user object provided with my own default values?
Thanks for your suggestions
Because the solution mention above by #Chopchop (thanks anyway for your effort) didn't work here I wrote a little workaround.
I created a new class called myController which extends Controller. The only function i override is the getUser() function. There I implement it like this:
public function getUser()
{
$user = Controller::getUser();
if ( !is_object($user) )
{
$user = new \ACME\myBundle\Entity\User();
$user->setUserLASTNAME ('RaRa');
$user->setID (0);
// etc...
}
return $user;
}
This works fine for me now. The only problem is that you really have to be careful NOT to forget to replace Controller by myController in all your *Controller.php files. So, better suggestions still welcome.
Works in Symfony 3.3
Using the suggestion of #Sfblaauw, I came up with a solution that uses a CompilerPass.
AppBundle/AppBundle.php
class AppBundle extends Bundle
{
public function build(ContainerBuilder $container)
{
parent::build($container);
$container->addCompilerPass(new OverrideAnonymousUserCompilerPass());
}
}
OverrideAnonymousUserCompilerPass.php
class OverrideAnonymousCompilerPass implements CompilerPassInterface
{
public function process(ContainerBuilder $container)
{
$definition = $container->getDefinition('security.authentication.listener.anonymous');
$definition->setClass(AnonymousAuthenticationListener::class);
}
}
AnonymousAuthenticationListener.php
class AnonymousAuthenticationListener implements ListenerInterface
{
private $tokenStorage;
private $secret;
private $authenticationManager;
private $logger;
public function __construct(TokenStorageInterface $tokenStorage, $secret, LoggerInterface $logger = null, AuthenticationManagerInterface $authenticationManager = null)
{
$this->tokenStorage = $tokenStorage;
$this->secret = $secret;
$this->authenticationManager = $authenticationManager;
$this->logger = $logger;
}
public function handle(GetResponseEvent $event)
{
if (null !== $this->tokenStorage->getToken()) {
return;
}
try {
// This is the important line:
$token = new AnonymousToken($this->secret, new AnonymousUser(), array());
if (null !== $this->authenticationManager) {
$token = $this->authenticationManager->authenticate($token);
}
$this->tokenStorage->setToken($token);
if (null !== $this->logger) {
$this->logger->info('Populated the TokenStorage with an anonymous Token.');
}
} catch (AuthenticationException $failed) {
if (null !== $this->logger) {
$this->logger->info('Anonymous authentication failed.', array('exception' => $failed));
}
}
}
}
This file is a copy of the AnonymousAuthenticationListener that comes with Symfony, but with the AnonymousToken constructor changed to pass in an AnonymousUser class instead of a string. In my case, AnonymousUser is a class that extends my User object, but you can implement it however you like.
These changes mean that {{ app.user }} in Twig and UserInterface injections in PHP will always return a User: you can use isinstance to tell if it's an AnonymousUser, or add a method isLoggedIn to your User class which returns true in User but false in AnonymousUser.
you can redirect the user not authenticated and force a fake login (to create a user ANONYMOUS)
and set it as well on logout
public function logoutAction(){
$em = $this->getDoctrine()->getManager();
$user = $em->getRepository('VendorBundle:User')->findByUserName('annonymous');
$session = $this->getRequest()->getSession();
$session->set('user', $user);
}
and if user is not set
public function checkLoginAction(){
if(!$session->get('user')){
$em = $this->getDoctrine()->getManager();
$user = $em->getRepository('VendorBundle:User')->findByUserName('annonymous');
$session = $this->getRequest()->getSession();
$session->set('user', $user);
}
//this->redirect('/');
}
in you security.yml
security:
firewalls:
main:
access_denied_url: /check_login/
access_control:
- { path: ^/$, role: ROLE_USER }
This is only an example i haven't tested (and will probably don't, since i don't get the purpose of doing this:) )
Using Symfony 2.6
Like Gordon says use the authentication listener to override the default anonymous user.
Now you can add the properties that you need to the anonymous user, in my case the language and the currency.
security.yml
parameters:
security.authentication.listener.anonymous.class: AppBundle\Security\Http\Firewall\AnonymousAuthenticationListener
AnonymousAuthenticationListener.php
namespace AppBundle\Security\Http\Firewall;
...
use AppBundle\Security\User\AnonymousUser;
class AnonymousAuthenticationListener implements ListenerInterface
{
...
public function handle(GetResponseEvent $event)
{
...
try {
$token = new AnonymousToken($this->key, new AnonymousUser(), array());
...
}
}
}
AnonymousUser.php
class AnonymousUser implements UserInterface
{
public function getUsername() { return 'anon.'; }
}

Categories